1
|
|
|
/*! |
2
|
|
|
* FullCalendar v2.1.1 |
3
|
|
|
* Docs & License: http://arshaw.com/fullcalendar/ |
4
|
|
|
* (c) 2013 Adam Shaw |
5
|
|
|
*/ |
6
|
|
|
|
7
|
|
|
(function(factory) { |
8
|
|
|
if (typeof define === 'function' && define.amd) { |
9
|
|
|
define([ 'jquery', 'moment' ], factory); |
10
|
|
|
} |
11
|
|
|
else { |
12
|
|
|
factory(jQuery, moment); |
13
|
|
|
} |
14
|
|
|
})(function($, moment) { |
15
|
|
|
|
16
|
|
|
;; |
17
|
|
|
|
18
|
|
|
var defaults = { |
19
|
|
|
|
20
|
|
|
lang: 'en', |
21
|
|
|
|
22
|
|
|
defaultTimedEventDuration: '02:00:00', |
23
|
|
|
defaultAllDayEventDuration: { days: 1 }, |
24
|
|
|
forceEventDuration: false, |
25
|
|
|
nextDayThreshold: '09:00:00', // 9am |
26
|
|
|
|
27
|
|
|
// display |
28
|
|
|
defaultView: 'month', |
29
|
|
|
aspectRatio: 1.35, |
30
|
|
|
header: { |
31
|
|
|
left: 'title', |
32
|
|
|
center: '', |
33
|
|
|
right: 'today prev,next' |
34
|
|
|
}, |
35
|
|
|
weekends: true, |
36
|
|
|
weekNumbers: false, |
37
|
|
|
|
38
|
|
|
weekNumberTitle: 'W', |
39
|
|
|
weekNumberCalculation: 'local', |
40
|
|
|
|
41
|
|
|
//editable: false, |
42
|
|
|
|
43
|
|
|
// event ajax |
44
|
|
|
lazyFetching: true, |
45
|
|
|
startParam: 'start', |
46
|
|
|
endParam: 'end', |
47
|
|
|
timezoneParam: 'timezone', |
48
|
|
|
|
49
|
|
|
timezone: false, |
50
|
|
|
|
51
|
|
|
//allDayDefault: undefined, |
52
|
|
|
|
53
|
|
|
// time formats |
54
|
|
|
titleFormat: { |
55
|
|
|
month: 'MMMM YYYY', // like "September 1986". each language will override this |
56
|
|
|
week: 'll', // like "Sep 4 1986" |
57
|
|
|
day: 'LL' // like "September 4 1986" |
58
|
|
|
}, |
59
|
|
|
columnFormat: { |
60
|
|
|
month: 'ddd', // like "Sat" |
61
|
|
|
week: generateWeekColumnFormat, |
62
|
|
|
day: 'dddd' // like "Saturday" |
63
|
|
|
}, |
64
|
|
|
timeFormat: { // for event elements |
65
|
|
|
'default': generateShortTimeFormat |
66
|
|
|
}, |
67
|
|
|
|
68
|
|
|
displayEventEnd: { |
69
|
|
|
month: false, |
70
|
|
|
basicWeek: false, |
71
|
|
|
'default': true |
72
|
|
|
}, |
73
|
|
|
|
74
|
|
|
// locale |
75
|
|
|
isRTL: false, |
76
|
|
|
defaultButtonText: { |
77
|
|
|
prev: "prev", |
78
|
|
|
next: "next", |
79
|
|
|
prevYear: "prev year", |
80
|
|
|
nextYear: "next year", |
81
|
|
|
today: 'today', |
82
|
|
|
month: 'month', |
83
|
|
|
week: 'week', |
84
|
|
|
day: 'day' |
85
|
|
|
}, |
86
|
|
|
|
87
|
|
|
buttonIcons: { |
88
|
|
|
prev: 'left-single-arrow', |
89
|
|
|
next: 'right-single-arrow', |
90
|
|
|
prevYear: 'left-double-arrow', |
91
|
|
|
nextYear: 'right-double-arrow' |
92
|
|
|
}, |
93
|
|
|
|
94
|
|
|
// jquery-ui theming |
95
|
|
|
theme: false, |
96
|
|
|
themeButtonIcons: { |
97
|
|
|
prev: 'circle-triangle-w', |
98
|
|
|
next: 'circle-triangle-e', |
99
|
|
|
prevYear: 'seek-prev', |
100
|
|
|
nextYear: 'seek-next' |
101
|
|
|
}, |
102
|
|
|
|
103
|
|
|
dragOpacity: .75, |
104
|
|
|
dragRevertDuration: 500, |
105
|
|
|
dragScroll: true, |
106
|
|
|
|
107
|
|
|
//selectable: false, |
108
|
|
|
unselectAuto: true, |
109
|
|
|
|
110
|
|
|
dropAccept: '*', |
111
|
|
|
|
112
|
|
|
eventLimit: false, |
113
|
|
|
eventLimitText: 'more', |
114
|
|
|
eventLimitClick: 'popover', |
115
|
|
|
dayPopoverFormat: 'LL', |
116
|
|
|
|
117
|
|
|
handleWindowResize: true, |
118
|
|
|
windowResizeDelay: 200 // milliseconds before a rerender happens |
119
|
|
|
|
120
|
|
|
}; |
121
|
|
|
|
122
|
|
|
|
123
|
|
|
function generateShortTimeFormat(options, langData) { |
124
|
|
|
return langData.longDateFormat('LT') |
125
|
|
|
.replace(':mm', '(:mm)') |
126
|
|
|
.replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs |
127
|
|
|
.replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
|
131
|
|
|
function generateWeekColumnFormat(options, langData) { |
132
|
|
|
var format = langData.longDateFormat('L'); // for the format like "MM/DD/YYYY" |
133
|
|
|
format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); // strip the year off the edge, as well as other misc non-whitespace chars |
134
|
|
|
if (options.isRTL) { |
135
|
|
|
format += ' ddd'; // for RTL, add day-of-week to end |
136
|
|
|
} |
137
|
|
|
else { |
138
|
|
|
format = 'ddd ' + format; // for LTR, add day-of-week to beginning |
139
|
|
|
} |
140
|
|
|
return format; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
|
144
|
|
|
var langOptionHash = { |
145
|
|
|
en: { |
146
|
|
|
columnFormat: { |
147
|
|
|
week: 'ddd M/D' // override for english. different from the generated default, which is MM/DD |
148
|
|
|
}, |
149
|
|
|
dayPopoverFormat: 'dddd, MMMM D' |
150
|
|
|
} |
151
|
|
|
}; |
152
|
|
|
|
153
|
|
|
|
154
|
|
|
// right-to-left defaults |
155
|
|
|
var rtlDefaults = { |
156
|
|
|
header: { |
157
|
|
|
left: 'next,prev today', |
158
|
|
|
center: '', |
159
|
|
|
right: 'title' |
160
|
|
|
}, |
161
|
|
|
buttonIcons: { |
162
|
|
|
prev: 'right-single-arrow', |
163
|
|
|
next: 'left-single-arrow', |
164
|
|
|
prevYear: 'right-double-arrow', |
165
|
|
|
nextYear: 'left-double-arrow' |
166
|
|
|
}, |
167
|
|
|
themeButtonIcons: { |
168
|
|
|
prev: 'circle-triangle-e', |
169
|
|
|
next: 'circle-triangle-w', |
170
|
|
|
nextYear: 'seek-prev', |
171
|
|
|
prevYear: 'seek-next' |
172
|
|
|
} |
173
|
|
|
}; |
174
|
|
|
|
175
|
|
|
;; |
176
|
|
|
|
177
|
|
|
var fc = $.fullCalendar = { version: "2.1.1" }; |
178
|
|
|
var fcViews = fc.views = {}; |
179
|
|
|
|
180
|
|
|
|
181
|
|
|
$.fn.fullCalendar = function(options) { |
182
|
|
|
var args = Array.prototype.slice.call(arguments, 1); // for a possible method call |
183
|
|
|
var res = this; // what this function will return (this jQuery object by default) |
184
|
|
|
|
185
|
|
|
this.each(function(i, _element) { // loop each DOM element involved |
186
|
|
|
var element = $(_element); |
187
|
|
|
var calendar = element.data('fullCalendar'); // get the existing calendar object (if any) |
188
|
|
|
var singleRes; // the returned value of this single method call |
189
|
|
|
|
190
|
|
|
// a method call |
191
|
|
|
if (typeof options === 'string') { |
192
|
|
|
if (calendar && $.isFunction(calendar[options])) { |
193
|
|
|
singleRes = calendar[options].apply(calendar, args); |
194
|
|
|
if (!i) { |
195
|
|
|
res = singleRes; // record the first method call result |
196
|
|
|
} |
197
|
|
|
if (options === 'destroy') { // for the destroy method, must remove Calendar object data |
198
|
|
|
element.removeData('fullCalendar'); |
199
|
|
|
} |
200
|
|
|
} |
201
|
|
|
} |
202
|
|
|
// a new calendar initialization |
203
|
|
|
else if (!calendar) { // don't initialize twice |
204
|
|
|
calendar = new Calendar(element, options); |
205
|
|
|
element.data('fullCalendar', calendar); |
206
|
|
|
calendar.render(); |
207
|
|
|
} |
208
|
|
|
}); |
209
|
|
|
|
210
|
|
|
return res; |
211
|
|
|
}; |
212
|
|
|
|
213
|
|
|
|
214
|
|
|
// function for adding/overriding defaults |
215
|
|
|
function setDefaults(d) { |
216
|
|
|
mergeOptions(defaults, d); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
|
220
|
|
|
// Recursively combines option hash-objects. |
221
|
|
|
// Better than `$.extend(true, ...)` because arrays are not traversed/copied. |
222
|
|
|
// |
223
|
|
|
// called like: |
224
|
|
|
// mergeOptions(target, obj1, obj2, ...) |
225
|
|
|
// |
226
|
|
|
function mergeOptions(target) { |
227
|
|
|
|
228
|
|
|
function mergeIntoTarget(name, value) { |
229
|
|
|
if ($.isPlainObject(value) && $.isPlainObject(target[name]) && !isForcedAtomicOption(name)) { |
230
|
|
|
// merge into a new object to avoid destruction |
231
|
|
|
target[name] = mergeOptions({}, target[name], value); // combine. `value` object takes precedence |
232
|
|
|
} |
233
|
|
|
else if (value !== undefined) { // only use values that are set and not undefined |
234
|
|
|
target[name] = value; |
235
|
|
|
} |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
for (var i=1; i<arguments.length; i++) { |
239
|
|
|
$.each(arguments[i], mergeIntoTarget); |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
return target; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
|
246
|
|
|
// overcome sucky view-option-hash and option-merging behavior messing with options it shouldn't |
247
|
|
|
function isForcedAtomicOption(name) { |
248
|
|
|
// Any option that ends in "Time" or "Duration" is probably a Duration, |
249
|
|
|
// and these will commonly be specified as plain objects, which we don't want to mess up. |
250
|
|
|
return /(Time|Duration)$/.test(name); |
251
|
|
|
} |
252
|
|
|
// FIX: find a different solution for view-option-hashes and have a whitelist |
253
|
|
|
// for options that can be recursively merged. |
254
|
|
|
|
255
|
|
|
;; |
256
|
|
|
|
257
|
|
|
//var langOptionHash = {}; // initialized in defaults.js |
258
|
|
|
fc.langs = langOptionHash; // expose |
259
|
|
|
|
260
|
|
|
|
261
|
|
|
// Initialize jQuery UI Datepicker translations while using some of the translations |
262
|
|
|
// for our own purposes. Will set this as the default language for datepicker. |
263
|
|
|
// Called from a translation file. |
264
|
|
|
fc.datepickerLang = function(langCode, datepickerLangCode, options) { |
265
|
|
|
var langOptions = langOptionHash[langCode]; |
266
|
|
|
|
267
|
|
|
// initialize FullCalendar's lang hash for this language |
268
|
|
|
if (!langOptions) { |
269
|
|
|
langOptions = langOptionHash[langCode] = {}; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
// merge certain Datepicker options into FullCalendar's options |
273
|
|
|
mergeOptions(langOptions, { |
274
|
|
|
isRTL: options.isRTL, |
275
|
|
|
weekNumberTitle: options.weekHeader, |
276
|
|
|
titleFormat: { |
277
|
|
|
month: options.showMonthAfterYear ? |
278
|
|
|
'YYYY[' + options.yearSuffix + '] MMMM' : |
279
|
|
|
'MMMM YYYY[' + options.yearSuffix + ']' |
280
|
|
|
}, |
281
|
|
|
defaultButtonText: { |
282
|
|
|
// the translations sometimes wrongly contain HTML entities |
283
|
|
|
prev: stripHtmlEntities(options.prevText), |
284
|
|
|
next: stripHtmlEntities(options.nextText), |
285
|
|
|
today: stripHtmlEntities(options.currentText) |
286
|
|
|
} |
287
|
|
|
}); |
288
|
|
|
|
289
|
|
|
// is jQuery UI Datepicker is on the page? |
290
|
|
|
if ($.datepicker) { |
291
|
|
|
|
292
|
|
|
// Register the language data. |
293
|
|
|
// FullCalendar and MomentJS use language codes like "pt-br" but Datepicker |
294
|
|
|
// does it like "pt-BR" or if it doesn't have the language, maybe just "pt". |
295
|
|
|
// Make an alias so the language can be referenced either way. |
296
|
|
|
$.datepicker.regional[datepickerLangCode] = |
297
|
|
|
$.datepicker.regional[langCode] = // alias |
298
|
|
|
options; |
299
|
|
|
|
300
|
|
|
// Alias 'en' to the default language data. Do this every time. |
301
|
|
|
$.datepicker.regional.en = $.datepicker.regional['']; |
302
|
|
|
|
303
|
|
|
// Set as Datepicker's global defaults. |
304
|
|
|
$.datepicker.setDefaults(options); |
305
|
|
|
} |
306
|
|
|
}; |
307
|
|
|
|
308
|
|
|
|
309
|
|
|
// Sets FullCalendar-specific translations. Also sets the language as the global default. |
310
|
|
|
// Called from a translation file. |
311
|
|
|
fc.lang = function(langCode, options) { |
312
|
|
|
var langOptions; |
313
|
|
|
|
314
|
|
|
if (options) { |
315
|
|
|
langOptions = langOptionHash[langCode]; |
316
|
|
|
|
317
|
|
|
// initialize the hash for this language |
318
|
|
|
if (!langOptions) { |
319
|
|
|
langOptions = langOptionHash[langCode] = {}; |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
mergeOptions(langOptions, options || {}); |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
// set it as the default language for FullCalendar |
326
|
|
|
defaults.lang = langCode; |
327
|
|
|
}; |
328
|
|
|
;; |
329
|
|
|
|
330
|
|
|
|
331
|
|
|
function Calendar(element, instanceOptions) { |
332
|
|
|
var t = this; |
333
|
|
|
|
334
|
|
|
|
335
|
|
|
|
336
|
|
|
// Build options object |
337
|
|
|
// ----------------------------------------------------------------------------------- |
338
|
|
|
// Precedence (lowest to highest): defaults, rtlDefaults, langOptions, instanceOptions |
339
|
|
|
|
340
|
|
|
instanceOptions = instanceOptions || {}; |
341
|
|
|
|
342
|
|
|
var options = mergeOptions({}, defaults, instanceOptions); |
343
|
|
|
var langOptions; |
344
|
|
|
|
345
|
|
|
// determine language options |
346
|
|
|
if (options.lang in langOptionHash) { |
347
|
|
|
langOptions = langOptionHash[options.lang]; |
348
|
|
|
} |
349
|
|
|
else { |
350
|
|
|
langOptions = langOptionHash[defaults.lang]; |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
if (langOptions) { // if language options exist, rebuild... |
354
|
|
|
options = mergeOptions({}, defaults, langOptions, instanceOptions); |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
if (options.isRTL) { // is isRTL, rebuild... |
358
|
|
|
options = mergeOptions({}, defaults, rtlDefaults, langOptions || {}, instanceOptions); |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
|
362
|
|
|
|
363
|
|
|
// Exports |
364
|
|
|
// ----------------------------------------------------------------------------------- |
365
|
|
|
|
366
|
|
|
t.options = options; |
367
|
|
|
t.render = render; |
368
|
|
|
t.destroy = destroy; |
369
|
|
|
t.refetchEvents = refetchEvents; |
370
|
|
|
t.reportEvents = reportEvents; |
371
|
|
|
t.reportEventChange = reportEventChange; |
372
|
|
|
t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method |
373
|
|
|
t.changeView = changeView; |
374
|
|
|
t.select = select; |
375
|
|
|
t.unselect = unselect; |
376
|
|
|
t.prev = prev; |
377
|
|
|
t.next = next; |
378
|
|
|
t.prevYear = prevYear; |
379
|
|
|
t.nextYear = nextYear; |
380
|
|
|
t.today = today; |
381
|
|
|
t.gotoDate = gotoDate; |
382
|
|
|
t.incrementDate = incrementDate; |
383
|
|
|
t.zoomTo = zoomTo; |
384
|
|
|
t.getDate = getDate; |
385
|
|
|
t.getCalendar = getCalendar; |
386
|
|
|
t.getView = getView; |
387
|
|
|
t.option = option; |
388
|
|
|
t.trigger = trigger; |
389
|
|
|
|
390
|
|
|
|
391
|
|
|
|
392
|
|
|
// Language-data Internals |
393
|
|
|
// ----------------------------------------------------------------------------------- |
394
|
|
|
// Apply overrides to the current language's data |
395
|
|
|
|
396
|
|
|
|
397
|
|
|
// Returns moment's internal locale data. If doesn't exist, returns English. |
398
|
|
|
// Works with moment-pre-2.8 |
399
|
|
|
function getLocaleData(langCode) { |
400
|
|
|
var f = moment.localeData || moment.langData; |
401
|
|
|
return f.call(moment, langCode) || |
402
|
|
|
f.call(moment, 'en'); // the newer localData could return null, so fall back to en |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
|
406
|
|
|
var localeData = createObject(getLocaleData(options.lang)); // make a cheap copy |
407
|
|
|
|
408
|
|
|
if (options.monthNames) { |
409
|
|
|
localeData._months = options.monthNames; |
410
|
|
|
} |
411
|
|
|
if (options.monthNamesShort) { |
412
|
|
|
localeData._monthsShort = options.monthNamesShort; |
413
|
|
|
} |
414
|
|
|
if (options.dayNames) { |
415
|
|
|
localeData._weekdays = options.dayNames; |
416
|
|
|
} |
417
|
|
|
if (options.dayNamesShort) { |
418
|
|
|
localeData._weekdaysShort = options.dayNamesShort; |
419
|
|
|
} |
420
|
|
|
if (options.firstDay != null) { |
421
|
|
|
var _week = createObject(localeData._week); // _week: { dow: # } |
422
|
|
|
_week.dow = options.firstDay; |
423
|
|
|
localeData._week = _week; |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
|
427
|
|
|
|
428
|
|
|
// Calendar-specific Date Utilities |
429
|
|
|
// ----------------------------------------------------------------------------------- |
430
|
|
|
|
431
|
|
|
|
432
|
|
|
t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration); |
433
|
|
|
t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration); |
434
|
|
|
|
435
|
|
|
|
436
|
|
|
// Builds a moment using the settings of the current calendar: timezone and language. |
437
|
|
|
// Accepts anything the vanilla moment() constructor accepts. |
438
|
|
|
t.moment = function() { |
439
|
|
|
var mom; |
440
|
|
|
|
441
|
|
|
if (options.timezone === 'local') { |
442
|
|
|
mom = fc.moment.apply(null, arguments); |
443
|
|
|
|
444
|
|
|
// Force the moment to be local, because fc.moment doesn't guarantee it. |
445
|
|
|
if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone |
446
|
|
|
mom.local(); |
447
|
|
|
} |
448
|
|
|
} |
449
|
|
|
else if (options.timezone === 'UTC') { |
450
|
|
|
mom = fc.moment.utc.apply(null, arguments); // process as UTC |
451
|
|
|
} |
452
|
|
|
else { |
453
|
|
|
mom = fc.moment.parseZone.apply(null, arguments); // let the input decide the zone |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
if ('_locale' in mom) { // moment 2.8 and above |
457
|
|
|
mom._locale = localeData; |
458
|
|
|
} |
459
|
|
|
else { // pre-moment-2.8 |
460
|
|
|
mom._lang = localeData; |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
return mom; |
464
|
|
|
}; |
465
|
|
|
|
466
|
|
|
|
467
|
|
|
// Returns a boolean about whether or not the calendar knows how to calculate |
468
|
|
|
// the timezone offset of arbitrary dates in the current timezone. |
469
|
|
|
t.getIsAmbigTimezone = function() { |
470
|
|
|
return options.timezone !== 'local' && options.timezone !== 'UTC'; |
471
|
|
|
}; |
472
|
|
|
|
473
|
|
|
|
474
|
|
|
// Returns a copy of the given date in the current timezone of it is ambiguously zoned. |
475
|
|
|
// This will also give the date an unambiguous time. |
476
|
|
|
t.rezoneDate = function(date) { |
477
|
|
|
return t.moment(date.toArray()); |
478
|
|
|
}; |
479
|
|
|
|
480
|
|
|
|
481
|
|
|
// Returns a moment for the current date, as defined by the client's computer, |
482
|
|
|
// or overridden by the `now` option. |
483
|
|
|
t.getNow = function() { |
484
|
|
|
var now = options.now; |
485
|
|
|
if (typeof now === 'function') { |
486
|
|
|
now = now(); |
487
|
|
|
} |
488
|
|
|
return t.moment(now); |
489
|
|
|
}; |
490
|
|
|
|
491
|
|
|
|
492
|
|
|
// Calculates the week number for a moment according to the calendar's |
493
|
|
|
// `weekNumberCalculation` setting. |
494
|
|
|
t.calculateWeekNumber = function(mom) { |
495
|
|
|
var calc = options.weekNumberCalculation; |
496
|
|
|
|
497
|
|
|
if (typeof calc === 'function') { |
498
|
|
|
return calc(mom); |
499
|
|
|
} |
500
|
|
|
else if (calc === 'local') { |
501
|
|
|
return mom.week(); |
502
|
|
|
} |
503
|
|
|
else if (calc.toUpperCase() === 'ISO') { |
504
|
|
|
return mom.isoWeek(); |
505
|
|
|
} |
506
|
|
|
}; |
507
|
|
|
|
508
|
|
|
|
509
|
|
|
// Get an event's normalized end date. If not present, calculate it from the defaults. |
510
|
|
|
t.getEventEnd = function(event) { |
511
|
|
|
if (event.end) { |
512
|
|
|
return event.end.clone(); |
513
|
|
|
} |
514
|
|
|
else { |
515
|
|
|
return t.getDefaultEventEnd(event.allDay, event.start); |
516
|
|
|
} |
517
|
|
|
}; |
518
|
|
|
|
519
|
|
|
|
520
|
|
|
// Given an event's allDay status and start date, return swhat its fallback end date should be. |
521
|
|
|
t.getDefaultEventEnd = function(allDay, start) { // TODO: rename to computeDefaultEventEnd |
522
|
|
|
var end = start.clone(); |
523
|
|
|
|
524
|
|
|
if (allDay) { |
525
|
|
|
end.stripTime().add(t.defaultAllDayEventDuration); |
526
|
|
|
} |
527
|
|
|
else { |
528
|
|
|
end.add(t.defaultTimedEventDuration); |
529
|
|
|
} |
530
|
|
|
|
531
|
|
|
if (t.getIsAmbigTimezone()) { |
532
|
|
|
end.stripZone(); // we don't know what the tzo should be |
533
|
|
|
} |
534
|
|
|
|
535
|
|
|
return end; |
536
|
|
|
}; |
537
|
|
|
|
538
|
|
|
|
539
|
|
|
|
540
|
|
|
// Date-formatting Utilities |
541
|
|
|
// ----------------------------------------------------------------------------------- |
542
|
|
|
|
543
|
|
|
|
544
|
|
|
// Like the vanilla formatRange, but with calendar-specific settings applied. |
545
|
|
|
t.formatRange = function(m1, m2, formatStr) { |
546
|
|
|
|
547
|
|
|
// a function that returns a formatStr // TODO: in future, precompute this |
548
|
|
|
if (typeof formatStr === 'function') { |
549
|
|
|
formatStr = formatStr.call(t, options, localeData); |
550
|
|
|
} |
551
|
|
|
|
552
|
|
|
return formatRange(m1, m2, formatStr, null, options.isRTL); |
553
|
|
|
}; |
554
|
|
|
|
555
|
|
|
|
556
|
|
|
// Like the vanilla formatDate, but with calendar-specific settings applied. |
557
|
|
|
t.formatDate = function(mom, formatStr) { |
558
|
|
|
|
559
|
|
|
// a function that returns a formatStr // TODO: in future, precompute this |
560
|
|
|
if (typeof formatStr === 'function') { |
561
|
|
|
formatStr = formatStr.call(t, options, localeData); |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
return formatDate(mom, formatStr); |
565
|
|
|
}; |
566
|
|
|
|
567
|
|
|
|
568
|
|
|
|
569
|
|
|
// Imports |
570
|
|
|
// ----------------------------------------------------------------------------------- |
571
|
|
|
|
572
|
|
|
|
573
|
|
|
EventManager.call(t, options); |
574
|
|
|
var isFetchNeeded = t.isFetchNeeded; |
575
|
|
|
var fetchEvents = t.fetchEvents; |
576
|
|
|
|
577
|
|
|
|
578
|
|
|
|
579
|
|
|
// Locals |
580
|
|
|
// ----------------------------------------------------------------------------------- |
581
|
|
|
|
582
|
|
|
|
583
|
|
|
var _element = element[0]; |
584
|
|
|
var header; |
585
|
|
|
var headerElement; |
586
|
|
|
var content; |
587
|
|
|
var tm; // for making theme classes |
588
|
|
|
var currentView; |
589
|
|
|
var suggestedViewHeight; |
590
|
|
|
var windowResizeProxy; // wraps the windowResize function |
591
|
|
|
var ignoreWindowResize = 0; |
592
|
|
|
var date; |
593
|
|
|
var events = []; |
594
|
|
|
|
595
|
|
|
|
596
|
|
|
|
597
|
|
|
// Main Rendering |
598
|
|
|
// ----------------------------------------------------------------------------------- |
599
|
|
|
|
600
|
|
|
|
601
|
|
|
if (options.defaultDate != null) { |
602
|
|
|
date = t.moment(options.defaultDate); |
603
|
|
|
} |
604
|
|
|
else { |
605
|
|
|
date = t.getNow(); |
606
|
|
|
} |
607
|
|
|
|
608
|
|
|
|
609
|
|
|
function render(inc) { |
610
|
|
|
if (!content) { |
611
|
|
|
initialRender(); |
612
|
|
|
} |
613
|
|
|
else if (elementVisible()) { |
614
|
|
|
// mainly for the public API |
615
|
|
|
calcSize(); |
616
|
|
|
renderView(inc); |
617
|
|
|
} |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
|
621
|
|
|
function initialRender() { |
622
|
|
|
tm = options.theme ? 'ui' : 'fc'; |
|
|
|
|
623
|
|
|
element.addClass('fc'); |
624
|
|
|
|
625
|
|
|
if (options.isRTL) { |
626
|
|
|
element.addClass('fc-rtl'); |
627
|
|
|
} |
628
|
|
|
else { |
629
|
|
|
element.addClass('fc-ltr'); |
630
|
|
|
} |
631
|
|
|
|
632
|
|
|
if (options.theme) { |
633
|
|
|
element.addClass('ui-widget'); |
634
|
|
|
} |
635
|
|
|
else { |
636
|
|
|
element.addClass('fc-unthemed'); |
637
|
|
|
} |
638
|
|
|
|
639
|
|
|
content = $("<div class='fc-view-container'/>").prependTo(element); |
640
|
|
|
|
641
|
|
|
header = new Header(t, options); |
642
|
|
|
headerElement = header.render(); |
643
|
|
|
if (headerElement) { |
644
|
|
|
element.prepend(headerElement); |
645
|
|
|
} |
646
|
|
|
|
647
|
|
|
changeView(options.defaultView); |
648
|
|
|
|
649
|
|
|
if (options.handleWindowResize) { |
650
|
|
|
windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls |
651
|
|
|
$(window).resize(windowResizeProxy); |
652
|
|
|
} |
653
|
|
|
} |
654
|
|
|
|
655
|
|
|
|
656
|
|
|
function destroy() { |
657
|
|
|
|
658
|
|
|
if (currentView) { |
659
|
|
|
currentView.destroy(); |
660
|
|
|
} |
661
|
|
|
|
662
|
|
|
header.destroy(); |
663
|
|
|
content.remove(); |
664
|
|
|
element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget'); |
665
|
|
|
|
666
|
|
|
$(window).unbind('resize', windowResizeProxy); |
667
|
|
|
} |
668
|
|
|
|
669
|
|
|
|
670
|
|
|
function elementVisible() { |
671
|
|
|
return element.is(':visible'); |
672
|
|
|
} |
673
|
|
|
|
674
|
|
|
|
675
|
|
|
|
676
|
|
|
// View Rendering |
677
|
|
|
// ----------------------------------------------------------------------------------- |
678
|
|
|
|
679
|
|
|
|
680
|
|
|
function changeView(viewName) { |
681
|
|
|
renderView(0, viewName); |
682
|
|
|
} |
683
|
|
|
|
684
|
|
|
|
685
|
|
|
// Renders a view because of a date change, view-type change, or for the first time |
686
|
|
|
function renderView(delta, viewName) { |
687
|
|
|
ignoreWindowResize++; |
688
|
|
|
|
689
|
|
|
// if viewName is changing, destroy the old view |
690
|
|
|
if (currentView && viewName && currentView.name !== viewName) { |
691
|
|
|
header.deactivateButton(currentView.name); |
692
|
|
|
freezeContentHeight(); // prevent a scroll jump when view element is removed |
693
|
|
|
if (currentView.start) { // rendered before? |
694
|
|
|
currentView.destroy(); |
695
|
|
|
} |
696
|
|
|
currentView.el.remove(); |
697
|
|
|
currentView = null; |
698
|
|
|
} |
699
|
|
|
|
700
|
|
|
// if viewName changed, or the view was never created, create a fresh view |
701
|
|
|
if (!currentView && viewName) { |
702
|
|
|
currentView = new fcViews[viewName](t); |
703
|
|
|
currentView.el = $("<div class='fc-view fc-" + viewName + "-view' />").appendTo(content); |
704
|
|
|
header.activateButton(viewName); |
705
|
|
|
} |
706
|
|
|
|
707
|
|
|
if (currentView) { |
708
|
|
|
|
709
|
|
|
// let the view determine what the delta means |
710
|
|
|
if (delta) { |
711
|
|
|
date = currentView.incrementDate(date, delta); |
712
|
|
|
} |
713
|
|
|
|
714
|
|
|
// render or rerender the view |
715
|
|
|
if ( |
716
|
|
|
!currentView.start || // never rendered before |
717
|
|
|
delta || // explicit date window change |
718
|
|
|
!date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change |
719
|
|
|
) { |
720
|
|
|
if (elementVisible()) { |
721
|
|
|
|
722
|
|
|
freezeContentHeight(); |
723
|
|
|
if (currentView.start) { // rendered before? |
724
|
|
|
currentView.destroy(); |
725
|
|
|
} |
726
|
|
|
currentView.render(date); |
727
|
|
|
unfreezeContentHeight(); |
728
|
|
|
|
729
|
|
|
// need to do this after View::render, so dates are calculated |
730
|
|
|
updateTitle(); |
731
|
|
|
updateTodayButton(); |
732
|
|
|
|
733
|
|
|
getAndRenderEvents(); |
734
|
|
|
} |
735
|
|
|
} |
736
|
|
|
} |
737
|
|
|
|
738
|
|
|
unfreezeContentHeight(); // undo any lone freezeContentHeight calls |
739
|
|
|
ignoreWindowResize--; |
740
|
|
|
} |
741
|
|
|
|
742
|
|
|
|
743
|
|
|
|
744
|
|
|
// Resizing |
745
|
|
|
// ----------------------------------------------------------------------------------- |
746
|
|
|
|
747
|
|
|
|
748
|
|
|
t.getSuggestedViewHeight = function() { |
749
|
|
|
if (suggestedViewHeight === undefined) { |
750
|
|
|
calcSize(); |
751
|
|
|
} |
752
|
|
|
return suggestedViewHeight; |
753
|
|
|
}; |
754
|
|
|
|
755
|
|
|
|
756
|
|
|
t.isHeightAuto = function() { |
757
|
|
|
return options.contentHeight === 'auto' || options.height === 'auto'; |
758
|
|
|
}; |
759
|
|
|
|
760
|
|
|
|
761
|
|
|
function updateSize(shouldRecalc) { |
762
|
|
|
if (elementVisible()) { |
763
|
|
|
|
764
|
|
|
if (shouldRecalc) { |
765
|
|
|
_calcSize(); |
766
|
|
|
} |
767
|
|
|
|
768
|
|
|
ignoreWindowResize++; |
769
|
|
|
currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto() |
770
|
|
|
ignoreWindowResize--; |
771
|
|
|
|
772
|
|
|
return true; // signal success |
773
|
|
|
} |
774
|
|
|
} |
775
|
|
|
|
776
|
|
|
|
777
|
|
|
function calcSize() { |
778
|
|
|
if (elementVisible()) { |
779
|
|
|
_calcSize(); |
780
|
|
|
} |
781
|
|
|
} |
782
|
|
|
|
783
|
|
|
|
784
|
|
|
function _calcSize() { // assumes elementVisible |
785
|
|
|
if (typeof options.contentHeight === 'number') { // exists and not 'auto' |
786
|
|
|
suggestedViewHeight = options.contentHeight; |
787
|
|
|
} |
788
|
|
|
else if (typeof options.height === 'number') { // exists and not 'auto' |
789
|
|
|
suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0); |
790
|
|
|
} |
791
|
|
|
else { |
792
|
|
|
suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); |
793
|
|
|
} |
794
|
|
|
} |
795
|
|
|
|
796
|
|
|
|
797
|
|
|
function windowResize(ev) { |
798
|
|
|
if ( |
799
|
|
|
!ignoreWindowResize && |
800
|
|
|
ev.target === window && // so we don't process jqui "resize" events that have bubbled up |
801
|
|
|
currentView.start // view has already been rendered |
802
|
|
|
) { |
803
|
|
|
if (updateSize(true)) { |
804
|
|
|
currentView.trigger('windowResize', _element); |
805
|
|
|
} |
806
|
|
|
} |
807
|
|
|
} |
808
|
|
|
|
809
|
|
|
|
810
|
|
|
|
811
|
|
|
/* Event Fetching/Rendering |
812
|
|
|
-----------------------------------------------------------------------------*/ |
813
|
|
|
// TODO: going forward, most of this stuff should be directly handled by the view |
814
|
|
|
|
815
|
|
|
|
816
|
|
|
function refetchEvents() { // can be called as an API method |
817
|
|
|
destroyEvents(); // so that events are cleared before user starts waiting for AJAX |
818
|
|
|
fetchAndRenderEvents(); |
819
|
|
|
} |
820
|
|
|
|
821
|
|
|
|
822
|
|
|
function renderEvents() { // destroys old events if previously rendered |
823
|
|
|
if (elementVisible()) { |
824
|
|
|
freezeContentHeight(); |
825
|
|
|
currentView.destroyEvents(); // no performance cost if never rendered |
826
|
|
|
currentView.renderEvents(events); |
827
|
|
|
unfreezeContentHeight(); |
828
|
|
|
} |
829
|
|
|
} |
830
|
|
|
|
831
|
|
|
|
832
|
|
|
function destroyEvents() { |
833
|
|
|
freezeContentHeight(); |
834
|
|
|
currentView.destroyEvents(); |
835
|
|
|
unfreezeContentHeight(); |
836
|
|
|
} |
837
|
|
|
|
838
|
|
|
|
839
|
|
|
function getAndRenderEvents() { |
840
|
|
|
if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) { |
841
|
|
|
fetchAndRenderEvents(); |
842
|
|
|
} |
843
|
|
|
else { |
844
|
|
|
renderEvents(); |
845
|
|
|
} |
846
|
|
|
} |
847
|
|
|
|
848
|
|
|
|
849
|
|
|
function fetchAndRenderEvents() { |
850
|
|
|
fetchEvents(currentView.start, currentView.end); |
851
|
|
|
// ... will call reportEvents |
852
|
|
|
// ... which will call renderEvents |
853
|
|
|
} |
854
|
|
|
|
855
|
|
|
|
856
|
|
|
// called when event data arrives |
857
|
|
|
function reportEvents(_events) { |
858
|
|
|
events = _events; |
859
|
|
|
renderEvents(); |
860
|
|
|
} |
861
|
|
|
|
862
|
|
|
|
863
|
|
|
// called when a single event's data has been changed |
864
|
|
|
function reportEventChange() { |
865
|
|
|
renderEvents(); |
866
|
|
|
} |
867
|
|
|
|
868
|
|
|
|
869
|
|
|
|
870
|
|
|
/* Header Updating |
871
|
|
|
-----------------------------------------------------------------------------*/ |
872
|
|
|
|
873
|
|
|
|
874
|
|
|
function updateTitle() { |
875
|
|
|
header.updateTitle(currentView.title); |
876
|
|
|
} |
877
|
|
|
|
878
|
|
|
|
879
|
|
|
function updateTodayButton() { |
880
|
|
|
var now = t.getNow(); |
881
|
|
|
if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) { |
882
|
|
|
header.disableButton('today'); |
883
|
|
|
} |
884
|
|
|
else { |
885
|
|
|
header.enableButton('today'); |
886
|
|
|
} |
887
|
|
|
} |
888
|
|
|
|
889
|
|
|
|
890
|
|
|
|
891
|
|
|
/* Selection |
892
|
|
|
-----------------------------------------------------------------------------*/ |
893
|
|
|
|
894
|
|
|
|
895
|
|
|
function select(start, end) { |
896
|
|
|
|
897
|
|
|
start = t.moment(start); |
898
|
|
|
if (end) { |
899
|
|
|
end = t.moment(end); |
900
|
|
|
} |
901
|
|
|
else if (start.hasTime()) { |
902
|
|
|
end = start.clone().add(t.defaultTimedEventDuration); |
903
|
|
|
} |
904
|
|
|
else { |
905
|
|
|
end = start.clone().add(t.defaultAllDayEventDuration); |
906
|
|
|
} |
907
|
|
|
|
908
|
|
|
currentView.select(start, end); |
909
|
|
|
} |
910
|
|
|
|
911
|
|
|
|
912
|
|
|
function unselect() { // safe to be called before renderView |
913
|
|
|
if (currentView) { |
914
|
|
|
currentView.unselect(); |
915
|
|
|
} |
916
|
|
|
} |
917
|
|
|
|
918
|
|
|
|
919
|
|
|
|
920
|
|
|
/* Date |
921
|
|
|
-----------------------------------------------------------------------------*/ |
922
|
|
|
|
923
|
|
|
|
924
|
|
|
function prev() { |
925
|
|
|
renderView(-1); |
926
|
|
|
} |
927
|
|
|
|
928
|
|
|
|
929
|
|
|
function next() { |
930
|
|
|
renderView(1); |
931
|
|
|
} |
932
|
|
|
|
933
|
|
|
|
934
|
|
|
function prevYear() { |
935
|
|
|
date.add(-1, 'years'); |
936
|
|
|
renderView(); |
937
|
|
|
} |
938
|
|
|
|
939
|
|
|
|
940
|
|
|
function nextYear() { |
941
|
|
|
date.add(1, 'years'); |
942
|
|
|
renderView(); |
943
|
|
|
} |
944
|
|
|
|
945
|
|
|
|
946
|
|
|
function today() { |
947
|
|
|
date = t.getNow(); |
948
|
|
|
renderView(); |
949
|
|
|
} |
950
|
|
|
|
951
|
|
|
|
952
|
|
|
function gotoDate(dateInput) { |
953
|
|
|
date = t.moment(dateInput); |
954
|
|
|
renderView(); |
955
|
|
|
} |
956
|
|
|
|
957
|
|
|
|
958
|
|
|
function incrementDate(delta) { |
959
|
|
|
date.add(moment.duration(delta)); |
960
|
|
|
renderView(); |
961
|
|
|
} |
962
|
|
|
|
963
|
|
|
|
964
|
|
|
// Forces navigation to a view for the given date. |
965
|
|
|
// `viewName` can be a specific view name or a generic one like "week" or "day". |
966
|
|
|
function zoomTo(newDate, viewName) { |
967
|
|
|
var viewStr; |
968
|
|
|
var match; |
969
|
|
|
|
970
|
|
|
if (!viewName || fcViews[viewName] === undefined) { // a general view name, or "auto" |
971
|
|
|
viewName = viewName || 'day'; |
972
|
|
|
viewStr = header.getViewsWithButtons().join(' '); // space-separated string of all the views in the header |
973
|
|
|
|
974
|
|
|
// try to match a general view name, like "week", against a specific one, like "agendaWeek" |
975
|
|
|
match = viewStr.match(new RegExp('\\w+' + capitaliseFirstLetter(viewName))); |
976
|
|
|
|
977
|
|
|
// fall back to the day view being used in the header |
978
|
|
|
if (!match) { |
979
|
|
|
match = viewStr.match(/\w+Day/); |
980
|
|
|
} |
981
|
|
|
|
982
|
|
|
viewName = match ? match[0] : 'agendaDay'; // fall back to agendaDay |
983
|
|
|
} |
984
|
|
|
|
985
|
|
|
date = newDate; |
986
|
|
|
changeView(viewName); |
987
|
|
|
} |
988
|
|
|
|
989
|
|
|
|
990
|
|
|
function getDate() { |
991
|
|
|
return date.clone(); |
992
|
|
|
} |
993
|
|
|
|
994
|
|
|
|
995
|
|
|
|
996
|
|
|
/* Height "Freezing" |
997
|
|
|
-----------------------------------------------------------------------------*/ |
998
|
|
|
|
999
|
|
|
|
1000
|
|
|
function freezeContentHeight() { |
1001
|
|
|
content.css({ |
1002
|
|
|
width: '100%', |
1003
|
|
|
height: content.height(), |
1004
|
|
|
overflow: 'hidden' |
1005
|
|
|
}); |
1006
|
|
|
} |
1007
|
|
|
|
1008
|
|
|
|
1009
|
|
|
function unfreezeContentHeight() { |
1010
|
|
|
content.css({ |
1011
|
|
|
width: '', |
1012
|
|
|
height: '', |
1013
|
|
|
overflow: '' |
1014
|
|
|
}); |
1015
|
|
|
} |
1016
|
|
|
|
1017
|
|
|
|
1018
|
|
|
|
1019
|
|
|
/* Misc |
1020
|
|
|
-----------------------------------------------------------------------------*/ |
1021
|
|
|
|
1022
|
|
|
|
1023
|
|
|
function getCalendar() { |
1024
|
|
|
return t; |
1025
|
|
|
} |
1026
|
|
|
|
1027
|
|
|
|
1028
|
|
|
function getView() { |
1029
|
|
|
return currentView; |
1030
|
|
|
} |
1031
|
|
|
|
1032
|
|
|
|
1033
|
|
|
function option(name, value) { |
1034
|
|
|
if (value === undefined) { |
1035
|
|
|
return options[name]; |
1036
|
|
|
} |
1037
|
|
|
if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') { |
1038
|
|
|
options[name] = value; |
1039
|
|
|
updateSize(true); // true = allow recalculation of height |
|
|
|
|
1040
|
|
|
} |
1041
|
|
|
} |
1042
|
|
|
|
1043
|
|
|
|
1044
|
|
|
function trigger(name, thisObj) { |
1045
|
|
|
if (options[name]) { |
1046
|
|
|
return options[name].apply( |
1047
|
|
|
thisObj || _element, |
1048
|
|
|
Array.prototype.slice.call(arguments, 2) |
1049
|
|
|
); |
1050
|
|
|
} |
1051
|
|
|
} |
1052
|
|
|
|
1053
|
|
|
} |
1054
|
|
|
|
1055
|
|
|
;; |
1056
|
|
|
|
1057
|
|
|
/* Top toolbar area with buttons and title |
1058
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
1059
|
|
|
// TODO: rename all header-related things to "toolbar" |
1060
|
|
|
|
1061
|
|
|
function Header(calendar, options) { |
1062
|
|
|
var t = this; |
1063
|
|
|
|
1064
|
|
|
// exports |
1065
|
|
|
t.render = render; |
1066
|
|
|
t.destroy = destroy; |
1067
|
|
|
t.updateTitle = updateTitle; |
1068
|
|
|
t.activateButton = activateButton; |
1069
|
|
|
t.deactivateButton = deactivateButton; |
1070
|
|
|
t.disableButton = disableButton; |
1071
|
|
|
t.enableButton = enableButton; |
1072
|
|
|
t.getViewsWithButtons = getViewsWithButtons; |
1073
|
|
|
|
1074
|
|
|
// locals |
1075
|
|
|
var el = $(); |
1076
|
|
|
var viewsWithButtons = []; |
1077
|
|
|
var tm; |
1078
|
|
|
|
1079
|
|
|
|
1080
|
|
|
function render() { |
1081
|
|
|
var sections = options.header; |
1082
|
|
|
|
1083
|
|
|
tm = options.theme ? 'ui' : 'fc'; |
1084
|
|
|
|
1085
|
|
|
if (sections) { |
1086
|
|
|
el = $("<div class='fc-toolbar'/>") |
1087
|
|
|
.append(renderSection('left')) |
1088
|
|
|
.append(renderSection('right')) |
1089
|
|
|
.append(renderSection('center')) |
1090
|
|
|
.append('<div class="fc-clear"/>'); |
1091
|
|
|
|
1092
|
|
|
return el; |
1093
|
|
|
} |
1094
|
|
|
} |
1095
|
|
|
|
1096
|
|
|
|
1097
|
|
|
function destroy() { |
1098
|
|
|
el.remove(); |
1099
|
|
|
} |
1100
|
|
|
|
1101
|
|
|
|
1102
|
|
|
function renderSection(position) { |
1103
|
|
|
var sectionEl = $('<div class="fc-' + position + '"/>'); |
1104
|
|
|
var buttonStr = options.header[position]; |
1105
|
|
|
|
1106
|
|
|
if (buttonStr) { |
1107
|
|
|
$.each(buttonStr.split(' '), function(i) { |
1108
|
|
|
var groupChildren = $(); |
1109
|
|
|
var isOnlyButtons = true; |
1110
|
|
|
var groupEl; |
1111
|
|
|
|
1112
|
|
|
$.each(this.split(','), function(j, buttonName) { |
1113
|
|
|
var buttonClick; |
1114
|
|
|
var themeIcon; |
1115
|
|
|
var normalIcon; |
1116
|
|
|
var defaultText; |
1117
|
|
|
var customText; |
1118
|
|
|
var innerHtml; |
1119
|
|
|
var classes; |
1120
|
|
|
var button; |
1121
|
|
|
|
1122
|
|
|
if (buttonName == 'title') { |
1123
|
|
|
groupChildren = groupChildren.add($('<h2> </h2>')); // we always want it to take up height |
1124
|
|
|
isOnlyButtons = false; |
1125
|
|
|
} |
1126
|
|
|
else { |
1127
|
|
|
if (calendar[buttonName]) { // a calendar method |
1128
|
|
|
buttonClick = function() { |
1129
|
|
|
calendar[buttonName](); |
1130
|
|
|
}; |
1131
|
|
|
} |
1132
|
|
|
else if (fcViews[buttonName]) { // a view name |
1133
|
|
|
buttonClick = function() { |
1134
|
|
|
calendar.changeView(buttonName); |
1135
|
|
|
}; |
1136
|
|
|
viewsWithButtons.push(buttonName); |
1137
|
|
|
} |
1138
|
|
|
if (buttonClick) { |
1139
|
|
|
|
1140
|
|
|
// smartProperty allows different text per view button (ex: "Agenda Week" vs "Basic Week") |
1141
|
|
|
themeIcon = smartProperty(options.themeButtonIcons, buttonName); |
1142
|
|
|
normalIcon = smartProperty(options.buttonIcons, buttonName); |
1143
|
|
|
defaultText = smartProperty(options.defaultButtonText, buttonName); |
1144
|
|
|
customText = smartProperty(options.buttonText, buttonName); |
1145
|
|
|
|
1146
|
|
|
if (customText) { |
1147
|
|
|
innerHtml = htmlEscape(customText); |
1148
|
|
|
} |
1149
|
|
|
else if (themeIcon && options.theme) { |
1150
|
|
|
innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>"; |
1151
|
|
|
} |
1152
|
|
|
else if (normalIcon && !options.theme) { |
1153
|
|
|
innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>"; |
1154
|
|
|
} |
1155
|
|
|
else { |
1156
|
|
|
innerHtml = htmlEscape(defaultText || buttonName); |
1157
|
|
|
} |
1158
|
|
|
|
1159
|
|
|
classes = [ |
1160
|
|
|
'fc-' + buttonName + '-button', |
1161
|
|
|
tm + '-button', |
1162
|
|
|
tm + '-state-default' |
1163
|
|
|
]; |
1164
|
|
|
|
1165
|
|
|
button = $( // type="button" so that it doesn't submit a form |
1166
|
|
|
'<button type="button" class="' + classes.join(' ') + '">' + |
1167
|
|
|
innerHtml + |
1168
|
|
|
'</button>' |
1169
|
|
|
) |
1170
|
|
|
.click(function() { |
1171
|
|
|
// don't process clicks for disabled buttons |
1172
|
|
|
if (!button.hasClass(tm + '-state-disabled')) { |
1173
|
|
|
|
1174
|
|
|
buttonClick(); |
1175
|
|
|
|
1176
|
|
|
// after the click action, if the button becomes the "active" tab, or disabled, |
1177
|
|
|
// it should never have a hover class, so remove it now. |
1178
|
|
|
if ( |
1179
|
|
|
button.hasClass(tm + '-state-active') || |
1180
|
|
|
button.hasClass(tm + '-state-disabled') |
1181
|
|
|
) { |
1182
|
|
|
button.removeClass(tm + '-state-hover'); |
1183
|
|
|
} |
1184
|
|
|
} |
1185
|
|
|
}) |
1186
|
|
|
.mousedown(function() { |
1187
|
|
|
// the *down* effect (mouse pressed in). |
1188
|
|
|
// only on buttons that are not the "active" tab, or disabled |
1189
|
|
|
button |
1190
|
|
|
.not('.' + tm + '-state-active') |
1191
|
|
|
.not('.' + tm + '-state-disabled') |
1192
|
|
|
.addClass(tm + '-state-down'); |
1193
|
|
|
}) |
1194
|
|
|
.mouseup(function() { |
1195
|
|
|
// undo the *down* effect |
1196
|
|
|
button.removeClass(tm + '-state-down'); |
1197
|
|
|
}) |
1198
|
|
|
.hover( |
1199
|
|
|
function() { |
1200
|
|
|
// the *hover* effect. |
1201
|
|
|
// only on buttons that are not the "active" tab, or disabled |
1202
|
|
|
button |
1203
|
|
|
.not('.' + tm + '-state-active') |
1204
|
|
|
.not('.' + tm + '-state-disabled') |
1205
|
|
|
.addClass(tm + '-state-hover'); |
1206
|
|
|
}, |
1207
|
|
|
function() { |
1208
|
|
|
// undo the *hover* effect |
1209
|
|
|
button |
1210
|
|
|
.removeClass(tm + '-state-hover') |
1211
|
|
|
.removeClass(tm + '-state-down'); // if mouseleave happens before mouseup |
1212
|
|
|
} |
1213
|
|
|
); |
1214
|
|
|
|
1215
|
|
|
groupChildren = groupChildren.add(button); |
1216
|
|
|
} |
1217
|
|
|
} |
1218
|
|
|
}); |
1219
|
|
|
|
1220
|
|
|
if (isOnlyButtons) { |
1221
|
|
|
groupChildren |
1222
|
|
|
.first().addClass(tm + '-corner-left').end() |
1223
|
|
|
.last().addClass(tm + '-corner-right').end(); |
1224
|
|
|
} |
1225
|
|
|
|
1226
|
|
|
if (groupChildren.length > 1) { |
1227
|
|
|
groupEl = $('<div/>'); |
1228
|
|
|
if (isOnlyButtons) { |
1229
|
|
|
groupEl.addClass('fc-button-group'); |
1230
|
|
|
} |
1231
|
|
|
groupEl.append(groupChildren); |
1232
|
|
|
sectionEl.append(groupEl); |
1233
|
|
|
} |
1234
|
|
|
else { |
1235
|
|
|
sectionEl.append(groupChildren); // 1 or 0 children |
1236
|
|
|
} |
1237
|
|
|
}); |
1238
|
|
|
} |
1239
|
|
|
|
1240
|
|
|
return sectionEl; |
1241
|
|
|
} |
1242
|
|
|
|
1243
|
|
|
|
1244
|
|
|
function updateTitle(text) { |
1245
|
|
|
el.find('h2').text(text); |
1246
|
|
|
} |
1247
|
|
|
|
1248
|
|
|
|
1249
|
|
|
function activateButton(buttonName) { |
1250
|
|
|
el.find('.fc-' + buttonName + '-button') |
1251
|
|
|
.addClass(tm + '-state-active'); |
1252
|
|
|
} |
1253
|
|
|
|
1254
|
|
|
|
1255
|
|
|
function deactivateButton(buttonName) { |
1256
|
|
|
el.find('.fc-' + buttonName + '-button') |
1257
|
|
|
.removeClass(tm + '-state-active'); |
1258
|
|
|
} |
1259
|
|
|
|
1260
|
|
|
|
1261
|
|
|
function disableButton(buttonName) { |
1262
|
|
|
el.find('.fc-' + buttonName + '-button') |
1263
|
|
|
.attr('disabled', 'disabled') |
1264
|
|
|
.addClass(tm + '-state-disabled'); |
1265
|
|
|
} |
1266
|
|
|
|
1267
|
|
|
|
1268
|
|
|
function enableButton(buttonName) { |
1269
|
|
|
el.find('.fc-' + buttonName + '-button') |
1270
|
|
|
.removeAttr('disabled') |
1271
|
|
|
.removeClass(tm + '-state-disabled'); |
1272
|
|
|
} |
1273
|
|
|
|
1274
|
|
|
|
1275
|
|
|
function getViewsWithButtons() { |
1276
|
|
|
return viewsWithButtons; |
1277
|
|
|
} |
1278
|
|
|
|
1279
|
|
|
} |
1280
|
|
|
|
1281
|
|
|
;; |
1282
|
|
|
|
1283
|
|
|
fc.sourceNormalizers = []; |
1284
|
|
|
fc.sourceFetchers = []; |
1285
|
|
|
|
1286
|
|
|
var ajaxDefaults = { |
1287
|
|
|
dataType: 'json', |
1288
|
|
|
cache: false |
1289
|
|
|
}; |
1290
|
|
|
|
1291
|
|
|
var eventGUID = 1; |
1292
|
|
|
|
1293
|
|
|
|
1294
|
|
|
function EventManager(options) { // assumed to be a calendar |
1295
|
|
|
var t = this; |
1296
|
|
|
|
1297
|
|
|
|
1298
|
|
|
// exports |
1299
|
|
|
t.isFetchNeeded = isFetchNeeded; |
1300
|
|
|
t.fetchEvents = fetchEvents; |
1301
|
|
|
t.addEventSource = addEventSource; |
1302
|
|
|
t.removeEventSource = removeEventSource; |
1303
|
|
|
t.updateEvent = updateEvent; |
1304
|
|
|
t.renderEvent = renderEvent; |
1305
|
|
|
t.removeEvents = removeEvents; |
1306
|
|
|
t.clientEvents = clientEvents; |
1307
|
|
|
t.mutateEvent = mutateEvent; |
1308
|
|
|
|
1309
|
|
|
|
1310
|
|
|
// imports |
1311
|
|
|
var trigger = t.trigger; |
1312
|
|
|
var getView = t.getView; |
1313
|
|
|
var reportEvents = t.reportEvents; |
1314
|
|
|
var getEventEnd = t.getEventEnd; |
1315
|
|
|
|
1316
|
|
|
|
1317
|
|
|
// locals |
1318
|
|
|
var stickySource = { events: [] }; |
1319
|
|
|
var sources = [ stickySource ]; |
1320
|
|
|
var rangeStart, rangeEnd; |
1321
|
|
|
var currentFetchID = 0; |
1322
|
|
|
var pendingSourceCnt = 0; |
1323
|
|
|
var loadingLevel = 0; |
1324
|
|
|
var cache = []; |
1325
|
|
|
|
1326
|
|
|
|
1327
|
|
|
$.each( |
1328
|
|
|
(options.events ? [ options.events ] : []).concat(options.eventSources || []), |
1329
|
|
|
function(i, sourceInput) { |
1330
|
|
|
var source = buildEventSource(sourceInput); |
1331
|
|
|
if (source) { |
1332
|
|
|
sources.push(source); |
1333
|
|
|
} |
1334
|
|
|
} |
1335
|
|
|
); |
1336
|
|
|
|
1337
|
|
|
|
1338
|
|
|
|
1339
|
|
|
/* Fetching |
1340
|
|
|
-----------------------------------------------------------------------------*/ |
1341
|
|
|
|
1342
|
|
|
|
1343
|
|
|
function isFetchNeeded(start, end) { |
1344
|
|
|
return !rangeStart || // nothing has been fetched yet? |
1345
|
|
|
// or, a part of the new range is outside of the old range? (after normalizing) |
1346
|
|
|
start.clone().stripZone() < rangeStart.clone().stripZone() || |
1347
|
|
|
end.clone().stripZone() > rangeEnd.clone().stripZone(); |
1348
|
|
|
} |
1349
|
|
|
|
1350
|
|
|
|
1351
|
|
|
function fetchEvents(start, end) { |
1352
|
|
|
rangeStart = start; |
1353
|
|
|
rangeEnd = end; |
1354
|
|
|
cache = []; |
1355
|
|
|
var fetchID = ++currentFetchID; |
1356
|
|
|
var len = sources.length; |
1357
|
|
|
pendingSourceCnt = len; |
1358
|
|
|
for (var i=0; i<len; i++) { |
1359
|
|
|
fetchEventSource(sources[i], fetchID); |
1360
|
|
|
} |
1361
|
|
|
} |
1362
|
|
|
|
1363
|
|
|
|
1364
|
|
|
function fetchEventSource(source, fetchID) { |
1365
|
|
|
_fetchEventSource(source, function(events) { |
1366
|
|
|
var isArraySource = $.isArray(source.events); |
1367
|
|
|
var i; |
1368
|
|
|
var event; |
1369
|
|
|
|
1370
|
|
|
if (fetchID == currentFetchID) { |
1371
|
|
|
|
1372
|
|
|
if (events) { |
1373
|
|
|
for (i=0; i<events.length; i++) { |
1374
|
|
|
event = events[i]; |
1375
|
|
|
|
1376
|
|
|
// event array sources have already been convert to Event Objects |
1377
|
|
|
if (!isArraySource) { |
1378
|
|
|
event = buildEvent(event, source); |
1379
|
|
|
} |
1380
|
|
|
|
1381
|
|
|
if (event) { |
1382
|
|
|
cache.push(event); |
1383
|
|
|
} |
1384
|
|
|
} |
1385
|
|
|
} |
1386
|
|
|
|
1387
|
|
|
pendingSourceCnt--; |
1388
|
|
|
if (!pendingSourceCnt) { |
1389
|
|
|
reportEvents(cache); |
1390
|
|
|
} |
1391
|
|
|
} |
1392
|
|
|
}); |
1393
|
|
|
} |
1394
|
|
|
|
1395
|
|
|
|
1396
|
|
|
function _fetchEventSource(source, callback) { |
1397
|
|
|
var i; |
1398
|
|
|
var fetchers = fc.sourceFetchers; |
1399
|
|
|
var res; |
1400
|
|
|
|
1401
|
|
|
for (i=0; i<fetchers.length; i++) { |
1402
|
|
|
res = fetchers[i].call( |
1403
|
|
|
t, // this, the Calendar object |
1404
|
|
|
source, |
1405
|
|
|
rangeStart.clone(), |
1406
|
|
|
rangeEnd.clone(), |
1407
|
|
|
options.timezone, |
1408
|
|
|
callback |
1409
|
|
|
); |
1410
|
|
|
|
1411
|
|
|
if (res === true) { |
1412
|
|
|
// the fetcher is in charge. made its own async request |
1413
|
|
|
return; |
1414
|
|
|
} |
1415
|
|
|
else if (typeof res == 'object') { |
1416
|
|
|
// the fetcher returned a new source. process it |
1417
|
|
|
_fetchEventSource(res, callback); |
1418
|
|
|
return; |
1419
|
|
|
} |
1420
|
|
|
} |
1421
|
|
|
|
1422
|
|
|
var events = source.events; |
1423
|
|
|
if (events) { |
1424
|
|
|
if ($.isFunction(events)) { |
1425
|
|
|
pushLoading(); |
1426
|
|
|
events.call( |
1427
|
|
|
t, // this, the Calendar object |
1428
|
|
|
rangeStart.clone(), |
1429
|
|
|
rangeEnd.clone(), |
1430
|
|
|
options.timezone, |
1431
|
|
|
function(events) { |
1432
|
|
|
callback(events); |
1433
|
|
|
popLoading(); |
1434
|
|
|
} |
1435
|
|
|
); |
1436
|
|
|
} |
1437
|
|
|
else if ($.isArray(events)) { |
1438
|
|
|
callback(events); |
1439
|
|
|
} |
1440
|
|
|
else { |
1441
|
|
|
callback(); |
1442
|
|
|
} |
1443
|
|
|
}else{ |
1444
|
|
|
var url = source.url; |
1445
|
|
|
if (url) { |
1446
|
|
|
var success = source.success; |
1447
|
|
|
var error = source.error; |
1448
|
|
|
var complete = source.complete; |
1449
|
|
|
|
1450
|
|
|
// retrieve any outbound GET/POST $.ajax data from the options |
1451
|
|
|
var customData; |
1452
|
|
|
if ($.isFunction(source.data)) { |
1453
|
|
|
// supplied as a function that returns a key/value object |
1454
|
|
|
customData = source.data(); |
1455
|
|
|
} |
1456
|
|
|
else { |
1457
|
|
|
// supplied as a straight key/value object |
1458
|
|
|
customData = source.data; |
1459
|
|
|
} |
1460
|
|
|
|
1461
|
|
|
// use a copy of the custom data so we can modify the parameters |
1462
|
|
|
// and not affect the passed-in object. |
1463
|
|
|
var data = $.extend({}, customData || {}); |
1464
|
|
|
|
1465
|
|
|
var startParam = firstDefined(source.startParam, options.startParam); |
1466
|
|
|
var endParam = firstDefined(source.endParam, options.endParam); |
1467
|
|
|
var timezoneParam = firstDefined(source.timezoneParam, options.timezoneParam); |
1468
|
|
|
|
1469
|
|
|
if (startParam) { |
1470
|
|
|
data[startParam] = rangeStart.format(); |
1471
|
|
|
} |
1472
|
|
|
if (endParam) { |
1473
|
|
|
data[endParam] = rangeEnd.format(); |
1474
|
|
|
} |
1475
|
|
|
if (options.timezone && options.timezone != 'local') { |
1476
|
|
|
data[timezoneParam] = options.timezone; |
1477
|
|
|
} |
1478
|
|
|
|
1479
|
|
|
pushLoading(); |
1480
|
|
|
$.ajax($.extend({}, ajaxDefaults, source, { |
1481
|
|
|
data: data, |
1482
|
|
|
success: function(events) { |
1483
|
|
|
events = events || []; |
1484
|
|
|
var res = applyAll(success, this, arguments); |
1485
|
|
|
if ($.isArray(res)) { |
1486
|
|
|
events = res; |
1487
|
|
|
} |
1488
|
|
|
callback(events); |
1489
|
|
|
}, |
1490
|
|
|
error: function() { |
1491
|
|
|
applyAll(error, this, arguments); |
1492
|
|
|
callback(); |
1493
|
|
|
}, |
1494
|
|
|
complete: function() { |
1495
|
|
|
applyAll(complete, this, arguments); |
1496
|
|
|
popLoading(); |
1497
|
|
|
} |
1498
|
|
|
})); |
1499
|
|
|
}else{ |
1500
|
|
|
callback(); |
1501
|
|
|
} |
1502
|
|
|
} |
1503
|
|
|
} |
1504
|
|
|
|
1505
|
|
|
|
1506
|
|
|
|
1507
|
|
|
/* Sources |
1508
|
|
|
-----------------------------------------------------------------------------*/ |
1509
|
|
|
|
1510
|
|
|
|
1511
|
|
|
function addEventSource(sourceInput) { |
1512
|
|
|
var source = buildEventSource(sourceInput); |
1513
|
|
|
if (source) { |
1514
|
|
|
sources.push(source); |
1515
|
|
|
pendingSourceCnt++; |
1516
|
|
|
fetchEventSource(source, currentFetchID); // will eventually call reportEvents |
1517
|
|
|
} |
1518
|
|
|
} |
1519
|
|
|
|
1520
|
|
|
|
1521
|
|
|
function buildEventSource(sourceInput) { // will return undefined if invalid source |
1522
|
|
|
var normalizers = fc.sourceNormalizers; |
1523
|
|
|
var source; |
1524
|
|
|
var i; |
1525
|
|
|
|
1526
|
|
|
if ($.isFunction(sourceInput) || $.isArray(sourceInput)) { |
1527
|
|
|
source = { events: sourceInput }; |
1528
|
|
|
} |
1529
|
|
|
else if (typeof sourceInput === 'string') { |
1530
|
|
|
source = { url: sourceInput }; |
1531
|
|
|
} |
1532
|
|
|
else if (typeof sourceInput === 'object') { |
1533
|
|
|
source = $.extend({}, sourceInput); // shallow copy |
1534
|
|
|
} |
1535
|
|
|
|
1536
|
|
|
if (source) { |
1537
|
|
|
|
1538
|
|
|
// TODO: repeat code, same code for event classNames |
1539
|
|
|
if (source.className) { |
1540
|
|
|
if (typeof source.className === 'string') { |
1541
|
|
|
source.className = source.className.split(/\s+/); |
1542
|
|
|
} |
1543
|
|
|
// otherwise, assumed to be an array |
1544
|
|
|
} |
1545
|
|
|
else { |
1546
|
|
|
source.className = []; |
1547
|
|
|
} |
1548
|
|
|
|
1549
|
|
|
// for array sources, we convert to standard Event Objects up front |
1550
|
|
|
if ($.isArray(source.events)) { |
1551
|
|
|
source.origArray = source.events; // for removeEventSource |
1552
|
|
|
source.events = $.map(source.events, function(eventInput) { |
1553
|
|
|
return buildEvent(eventInput, source); |
1554
|
|
|
}); |
1555
|
|
|
} |
1556
|
|
|
|
1557
|
|
|
for (i=0; i<normalizers.length; i++) { |
1558
|
|
|
normalizers[i].call(t, source); |
1559
|
|
|
} |
1560
|
|
|
|
1561
|
|
|
return source; |
1562
|
|
|
} |
1563
|
|
|
} |
1564
|
|
|
|
1565
|
|
|
|
1566
|
|
|
function removeEventSource(source) { |
1567
|
|
|
sources = $.grep(sources, function(src) { |
1568
|
|
|
return !isSourcesEqual(src, source); |
1569
|
|
|
}); |
1570
|
|
|
// remove all client events from that source |
1571
|
|
|
cache = $.grep(cache, function(e) { |
1572
|
|
|
return !isSourcesEqual(e.source, source); |
1573
|
|
|
}); |
1574
|
|
|
reportEvents(cache); |
1575
|
|
|
} |
1576
|
|
|
|
1577
|
|
|
|
1578
|
|
|
function isSourcesEqual(source1, source2) { |
1579
|
|
|
return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2); |
1580
|
|
|
} |
1581
|
|
|
|
1582
|
|
|
|
1583
|
|
|
function getSourcePrimitive(source) { |
1584
|
|
|
return ( |
1585
|
|
|
(typeof source === 'object') ? // a normalized event source? |
1586
|
|
|
(source.origArray || source.url || source.events) : // get the primitive |
1587
|
|
|
null |
1588
|
|
|
) || |
1589
|
|
|
source; // the given argument *is* the primitive |
1590
|
|
|
} |
1591
|
|
|
|
1592
|
|
|
|
1593
|
|
|
|
1594
|
|
|
/* Manipulation |
1595
|
|
|
-----------------------------------------------------------------------------*/ |
1596
|
|
|
|
1597
|
|
|
|
1598
|
|
|
function updateEvent(event) { |
1599
|
|
|
|
1600
|
|
|
event.start = t.moment(event.start); |
1601
|
|
|
if (event.end) { |
1602
|
|
|
event.end = t.moment(event.end); |
1603
|
|
|
} |
1604
|
|
|
|
1605
|
|
|
mutateEvent(event); |
1606
|
|
|
propagateMiscProperties(event); |
1607
|
|
|
reportEvents(cache); // reports event modifications (so we can redraw) |
1608
|
|
|
} |
1609
|
|
|
|
1610
|
|
|
|
1611
|
|
|
var miscCopyableProps = [ |
1612
|
|
|
'title', |
1613
|
|
|
'url', |
1614
|
|
|
'allDay', |
1615
|
|
|
'className', |
1616
|
|
|
'editable', |
1617
|
|
|
'color', |
1618
|
|
|
'backgroundColor', |
1619
|
|
|
'borderColor', |
1620
|
|
|
'textColor' |
1621
|
|
|
]; |
1622
|
|
|
|
1623
|
|
|
function propagateMiscProperties(event) { |
1624
|
|
|
var i; |
1625
|
|
|
var cachedEvent; |
1626
|
|
|
var j; |
1627
|
|
|
var prop; |
1628
|
|
|
|
1629
|
|
|
for (i=0; i<cache.length; i++) { |
1630
|
|
|
cachedEvent = cache[i]; |
1631
|
|
|
if (cachedEvent._id == event._id && cachedEvent !== event) { |
1632
|
|
|
for (j=0; j<miscCopyableProps.length; j++) { |
1633
|
|
|
prop = miscCopyableProps[j]; |
1634
|
|
|
if (event[prop] !== undefined) { |
1635
|
|
|
cachedEvent[prop] = event[prop]; |
1636
|
|
|
} |
1637
|
|
|
} |
1638
|
|
|
} |
1639
|
|
|
} |
1640
|
|
|
} |
1641
|
|
|
|
1642
|
|
|
|
1643
|
|
|
|
1644
|
|
|
function renderEvent(eventData, stick) { |
1645
|
|
|
var event = buildEvent(eventData); |
1646
|
|
|
if (event) { |
1647
|
|
|
if (!event.source) { |
1648
|
|
|
if (stick) { |
1649
|
|
|
stickySource.events.push(event); |
1650
|
|
|
event.source = stickySource; |
1651
|
|
|
} |
1652
|
|
|
cache.push(event); |
1653
|
|
|
} |
1654
|
|
|
reportEvents(cache); |
1655
|
|
|
} |
1656
|
|
|
} |
1657
|
|
|
|
1658
|
|
|
|
1659
|
|
|
function removeEvents(filter) { |
1660
|
|
|
var eventID; |
1661
|
|
|
var i; |
1662
|
|
|
|
1663
|
|
|
if (filter == null) { // null or undefined. remove all events |
1664
|
|
|
filter = function() { return true; }; // will always match |
1665
|
|
|
} |
1666
|
|
|
else if (!$.isFunction(filter)) { // an event ID |
1667
|
|
|
eventID = filter + ''; |
1668
|
|
|
filter = function(event) { |
1669
|
|
|
return event._id == eventID; |
1670
|
|
|
}; |
1671
|
|
|
} |
1672
|
|
|
|
1673
|
|
|
// Purge event(s) from our local cache |
1674
|
|
|
cache = $.grep(cache, filter, true); // inverse=true |
1675
|
|
|
|
1676
|
|
|
// Remove events from array sources. |
1677
|
|
|
// This works because they have been converted to official Event Objects up front. |
1678
|
|
|
// (and as a result, event._id has been calculated). |
1679
|
|
|
for (i=0; i<sources.length; i++) { |
1680
|
|
|
if ($.isArray(sources[i].events)) { |
1681
|
|
|
sources[i].events = $.grep(sources[i].events, filter, true); |
1682
|
|
|
} |
1683
|
|
|
} |
1684
|
|
|
|
1685
|
|
|
reportEvents(cache); |
1686
|
|
|
} |
1687
|
|
|
|
1688
|
|
|
|
1689
|
|
|
function clientEvents(filter) { |
1690
|
|
|
if ($.isFunction(filter)) { |
1691
|
|
|
return $.grep(cache, filter); |
1692
|
|
|
} |
1693
|
|
|
else if (filter != null) { // not null, not undefined. an event ID |
1694
|
|
|
filter += ''; |
1695
|
|
|
return $.grep(cache, function(e) { |
1696
|
|
|
return e._id == filter; |
1697
|
|
|
}); |
1698
|
|
|
} |
1699
|
|
|
return cache; // else, return all |
1700
|
|
|
} |
1701
|
|
|
|
1702
|
|
|
|
1703
|
|
|
|
1704
|
|
|
/* Loading State |
1705
|
|
|
-----------------------------------------------------------------------------*/ |
1706
|
|
|
|
1707
|
|
|
|
1708
|
|
|
function pushLoading() { |
1709
|
|
|
if (!(loadingLevel++)) { |
1710
|
|
|
trigger('loading', null, true, getView()); |
1711
|
|
|
} |
1712
|
|
|
} |
1713
|
|
|
|
1714
|
|
|
|
1715
|
|
|
function popLoading() { |
1716
|
|
|
if (!(--loadingLevel)) { |
1717
|
|
|
trigger('loading', null, false, getView()); |
1718
|
|
|
} |
1719
|
|
|
} |
1720
|
|
|
|
1721
|
|
|
|
1722
|
|
|
|
1723
|
|
|
/* Event Normalization |
1724
|
|
|
-----------------------------------------------------------------------------*/ |
1725
|
|
|
|
1726
|
|
|
function buildEvent(data, source) { // source may be undefined! |
1727
|
|
|
var out = {}; |
1728
|
|
|
var start; |
1729
|
|
|
var end; |
1730
|
|
|
var allDay; |
1731
|
|
|
var allDayDefault; |
1732
|
|
|
|
1733
|
|
|
if (options.eventDataTransform) { |
1734
|
|
|
data = options.eventDataTransform(data); |
1735
|
|
|
} |
1736
|
|
|
if (source && source.eventDataTransform) { |
1737
|
|
|
data = source.eventDataTransform(data); |
1738
|
|
|
} |
1739
|
|
|
|
1740
|
|
|
start = t.moment(data.start || data.date); // "date" is an alias for "start" |
1741
|
|
|
if (!start.isValid()) { |
1742
|
|
|
return; |
|
|
|
|
1743
|
|
|
} |
1744
|
|
|
|
1745
|
|
|
end = null; |
1746
|
|
|
if (data.end) { |
1747
|
|
|
end = t.moment(data.end); |
1748
|
|
|
if (!end.isValid()) { |
1749
|
|
|
return; |
|
|
|
|
1750
|
|
|
} |
1751
|
|
|
} |
1752
|
|
|
|
1753
|
|
|
allDay = data.allDay; |
1754
|
|
|
if (allDay === undefined) { |
1755
|
|
|
allDayDefault = firstDefined( |
1756
|
|
|
source ? source.allDayDefault : undefined, |
1757
|
|
|
options.allDayDefault |
1758
|
|
|
); |
1759
|
|
|
if (allDayDefault !== undefined) { |
1760
|
|
|
// use the default |
1761
|
|
|
allDay = allDayDefault; |
1762
|
|
|
} |
1763
|
|
|
else { |
1764
|
|
|
// all dates need to have ambig time for the event to be considered allDay |
1765
|
|
|
allDay = !start.hasTime() && (!end || !end.hasTime()); |
1766
|
|
|
} |
1767
|
|
|
} |
1768
|
|
|
|
1769
|
|
|
// normalize the date based on allDay |
1770
|
|
|
if (allDay) { |
1771
|
|
|
// neither date should have a time |
1772
|
|
|
if (start.hasTime()) { |
1773
|
|
|
start.stripTime(); |
1774
|
|
|
} |
1775
|
|
|
if (end && end.hasTime()) { |
1776
|
|
|
end.stripTime(); |
1777
|
|
|
} |
1778
|
|
|
} |
1779
|
|
|
else { |
1780
|
|
|
// force a time/zone up the dates |
1781
|
|
|
if (!start.hasTime()) { |
1782
|
|
|
start = t.rezoneDate(start); |
1783
|
|
|
} |
1784
|
|
|
if (end && !end.hasTime()) { |
1785
|
|
|
end = t.rezoneDate(end); |
1786
|
|
|
} |
1787
|
|
|
} |
1788
|
|
|
|
1789
|
|
|
// Copy all properties over to the resulting object. |
1790
|
|
|
// The special-case properties will be copied over afterwards. |
1791
|
|
|
$.extend(out, data); |
1792
|
|
|
|
1793
|
|
|
if (source) { |
1794
|
|
|
out.source = source; |
1795
|
|
|
} |
1796
|
|
|
|
1797
|
|
|
out._id = data._id || (data.id === undefined ? '_fc' + eventGUID++ : data.id + ''); |
1798
|
|
|
|
1799
|
|
|
if (data.className) { |
1800
|
|
|
if (typeof data.className == 'string') { |
1801
|
|
|
out.className = data.className.split(/\s+/); |
1802
|
|
|
} |
1803
|
|
|
else { // assumed to be an array |
1804
|
|
|
out.className = data.className; |
1805
|
|
|
} |
1806
|
|
|
} |
1807
|
|
|
else { |
1808
|
|
|
out.className = []; |
1809
|
|
|
} |
1810
|
|
|
|
1811
|
|
|
out.allDay = allDay; |
1812
|
|
|
out.start = start; |
1813
|
|
|
out.end = end; |
1814
|
|
|
|
1815
|
|
|
if (options.forceEventDuration && !out.end) { |
1816
|
|
|
out.end = getEventEnd(out); |
1817
|
|
|
} |
1818
|
|
|
|
1819
|
|
|
backupEventDates(out); |
1820
|
|
|
|
1821
|
|
|
return out; |
1822
|
|
|
} |
1823
|
|
|
|
1824
|
|
|
|
1825
|
|
|
|
1826
|
|
|
/* Event Modification Math |
1827
|
|
|
-----------------------------------------------------------------------------------------*/ |
1828
|
|
|
|
1829
|
|
|
|
1830
|
|
|
// Modify the date(s) of an event and make this change propagate to all other events with |
1831
|
|
|
// the same ID (related repeating events). |
1832
|
|
|
// |
1833
|
|
|
// If `newStart`/`newEnd` are not specified, the "new" dates are assumed to be `event.start` and `event.end`. |
1834
|
|
|
// The "old" dates to be compare against are always `event._start` and `event._end` (set by EventManager). |
1835
|
|
|
// |
1836
|
|
|
// Returns an object with delta information and a function to undo all operations. |
1837
|
|
|
// |
1838
|
|
|
function mutateEvent(event, newStart, newEnd) { |
1839
|
|
|
var oldAllDay = event._allDay; |
1840
|
|
|
var oldStart = event._start; |
1841
|
|
|
var oldEnd = event._end; |
1842
|
|
|
var clearEnd = false; |
1843
|
|
|
var newAllDay; |
1844
|
|
|
var dateDelta; |
1845
|
|
|
var durationDelta; |
1846
|
|
|
var undoFunc; |
1847
|
|
|
|
1848
|
|
|
// if no new dates were passed in, compare against the event's existing dates |
1849
|
|
|
if (!newStart && !newEnd) { |
1850
|
|
|
newStart = event.start; |
1851
|
|
|
newEnd = event.end; |
1852
|
|
|
} |
1853
|
|
|
|
1854
|
|
|
// NOTE: throughout this function, the initial values of `newStart` and `newEnd` are |
1855
|
|
|
// preserved. These values may be undefined. |
1856
|
|
|
|
1857
|
|
|
// detect new allDay |
1858
|
|
|
if (event.allDay != oldAllDay) { // if value has changed, use it |
1859
|
|
|
newAllDay = event.allDay; |
1860
|
|
|
} |
1861
|
|
|
else { // otherwise, see if any of the new dates are allDay |
1862
|
|
|
newAllDay = !(newStart || newEnd).hasTime(); |
1863
|
|
|
} |
1864
|
|
|
|
1865
|
|
|
// normalize the new dates based on allDay |
1866
|
|
|
if (newAllDay) { |
1867
|
|
|
if (newStart) { |
1868
|
|
|
newStart = newStart.clone().stripTime(); |
1869
|
|
|
} |
1870
|
|
|
if (newEnd) { |
1871
|
|
|
newEnd = newEnd.clone().stripTime(); |
1872
|
|
|
} |
1873
|
|
|
} |
1874
|
|
|
|
1875
|
|
|
// compute dateDelta |
1876
|
|
|
if (newStart) { |
1877
|
|
|
if (newAllDay) { |
1878
|
|
|
dateDelta = dayishDiff(newStart, oldStart.clone().stripTime()); // treat oldStart as allDay |
1879
|
|
|
} |
1880
|
|
|
else { |
1881
|
|
|
dateDelta = dayishDiff(newStart, oldStart); |
1882
|
|
|
} |
1883
|
|
|
} |
1884
|
|
|
|
1885
|
|
|
if (newAllDay != oldAllDay) { |
1886
|
|
|
// if allDay has changed, always throw away the end |
1887
|
|
|
clearEnd = true; |
1888
|
|
|
} |
1889
|
|
|
else if (newEnd) { |
1890
|
|
|
durationDelta = dayishDiff( |
1891
|
|
|
// new duration |
1892
|
|
|
newEnd || t.getDefaultEventEnd(newAllDay, newStart || oldStart), |
1893
|
|
|
newStart || oldStart |
1894
|
|
|
).subtract(dayishDiff( |
1895
|
|
|
// subtract old duration |
1896
|
|
|
oldEnd || t.getDefaultEventEnd(oldAllDay, oldStart), |
1897
|
|
|
oldStart |
1898
|
|
|
)); |
1899
|
|
|
} |
1900
|
|
|
|
1901
|
|
|
undoFunc = mutateEvents( |
1902
|
|
|
clientEvents(event._id), // get events with this ID |
1903
|
|
|
clearEnd, |
1904
|
|
|
newAllDay, |
1905
|
|
|
dateDelta, |
|
|
|
|
1906
|
|
|
durationDelta |
|
|
|
|
1907
|
|
|
); |
1908
|
|
|
|
1909
|
|
|
return { |
1910
|
|
|
dateDelta: dateDelta, |
1911
|
|
|
durationDelta: durationDelta, |
1912
|
|
|
undo: undoFunc |
1913
|
|
|
}; |
1914
|
|
|
} |
1915
|
|
|
|
1916
|
|
|
|
1917
|
|
|
// Modifies an array of events in the following ways (operations are in order): |
1918
|
|
|
// - clear the event's `end` |
1919
|
|
|
// - convert the event to allDay |
1920
|
|
|
// - add `dateDelta` to the start and end |
1921
|
|
|
// - add `durationDelta` to the event's duration |
1922
|
|
|
// |
1923
|
|
|
// Returns a function that can be called to undo all the operations. |
1924
|
|
|
// |
1925
|
|
|
function mutateEvents(events, clearEnd, forceAllDay, dateDelta, durationDelta) { |
1926
|
|
|
var isAmbigTimezone = t.getIsAmbigTimezone(); |
1927
|
|
|
var undoFunctions = []; |
1928
|
|
|
|
1929
|
|
|
$.each(events, function(i, event) { |
1930
|
|
|
var oldAllDay = event._allDay; |
1931
|
|
|
var oldStart = event._start; |
1932
|
|
|
var oldEnd = event._end; |
1933
|
|
|
var newAllDay = forceAllDay != null ? forceAllDay : oldAllDay; |
1934
|
|
|
var newStart = oldStart.clone(); |
1935
|
|
|
var newEnd = (!clearEnd && oldEnd) ? oldEnd.clone() : null; |
1936
|
|
|
|
1937
|
|
|
// NOTE: this function is responsible for transforming `newStart` and `newEnd`, |
1938
|
|
|
// which were initialized to the OLD values first. `newEnd` may be null. |
1939
|
|
|
|
1940
|
|
|
// normlize newStart/newEnd to be consistent with newAllDay |
1941
|
|
|
if (newAllDay) { |
1942
|
|
|
newStart.stripTime(); |
1943
|
|
|
if (newEnd) { |
1944
|
|
|
newEnd.stripTime(); |
1945
|
|
|
} |
1946
|
|
|
} |
1947
|
|
|
else { |
1948
|
|
|
if (!newStart.hasTime()) { |
1949
|
|
|
newStart = t.rezoneDate(newStart); |
1950
|
|
|
} |
1951
|
|
|
if (newEnd && !newEnd.hasTime()) { |
1952
|
|
|
newEnd = t.rezoneDate(newEnd); |
1953
|
|
|
} |
1954
|
|
|
} |
1955
|
|
|
|
1956
|
|
|
// ensure we have an end date if necessary |
1957
|
|
|
if (!newEnd && (options.forceEventDuration || +durationDelta)) { |
1958
|
|
|
newEnd = t.getDefaultEventEnd(newAllDay, newStart); |
1959
|
|
|
} |
1960
|
|
|
|
1961
|
|
|
// translate the dates |
1962
|
|
|
newStart.add(dateDelta); |
1963
|
|
|
if (newEnd) { |
1964
|
|
|
newEnd.add(dateDelta).add(durationDelta); |
1965
|
|
|
} |
1966
|
|
|
|
1967
|
|
|
// if the dates have changed, and we know it is impossible to recompute the |
1968
|
|
|
// timezone offsets, strip the zone. |
1969
|
|
|
if (isAmbigTimezone) { |
1970
|
|
|
if (+dateDelta || +durationDelta) { |
1971
|
|
|
newStart.stripZone(); |
1972
|
|
|
if (newEnd) { |
1973
|
|
|
newEnd.stripZone(); |
1974
|
|
|
} |
1975
|
|
|
} |
1976
|
|
|
} |
1977
|
|
|
|
1978
|
|
|
event.allDay = newAllDay; |
1979
|
|
|
event.start = newStart; |
1980
|
|
|
event.end = newEnd; |
1981
|
|
|
backupEventDates(event); |
1982
|
|
|
|
1983
|
|
|
undoFunctions.push(function() { |
1984
|
|
|
event.allDay = oldAllDay; |
1985
|
|
|
event.start = oldStart; |
1986
|
|
|
event.end = oldEnd; |
1987
|
|
|
backupEventDates(event); |
1988
|
|
|
}); |
1989
|
|
|
}); |
1990
|
|
|
|
1991
|
|
|
return function() { |
1992
|
|
|
for (var i=0; i<undoFunctions.length; i++) { |
1993
|
|
|
undoFunctions[i](); |
1994
|
|
|
} |
1995
|
|
|
}; |
1996
|
|
|
} |
1997
|
|
|
|
1998
|
|
|
} |
1999
|
|
|
|
2000
|
|
|
|
2001
|
|
|
// updates the "backup" properties, which are preserved in order to compute diffs later on. |
2002
|
|
|
function backupEventDates(event) { |
2003
|
|
|
event._allDay = event.allDay; |
2004
|
|
|
event._start = event.start.clone(); |
2005
|
|
|
event._end = event.end ? event.end.clone() : null; |
2006
|
|
|
} |
2007
|
|
|
|
2008
|
|
|
;; |
2009
|
|
|
|
2010
|
|
|
/* FullCalendar-specific DOM Utilities |
2011
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
2012
|
|
|
|
2013
|
|
|
|
2014
|
|
|
// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left |
2015
|
|
|
// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that. |
2016
|
|
|
function compensateScroll(rowEls, scrollbarWidths) { |
2017
|
|
|
if (scrollbarWidths.left) { |
2018
|
|
|
rowEls.css({ |
2019
|
|
|
'border-left-width': 1, |
2020
|
|
|
'margin-left': scrollbarWidths.left - 1 |
2021
|
|
|
}); |
2022
|
|
|
} |
2023
|
|
|
if (scrollbarWidths.right) { |
2024
|
|
|
rowEls.css({ |
2025
|
|
|
'border-right-width': 1, |
2026
|
|
|
'margin-right': scrollbarWidths.right - 1 |
2027
|
|
|
}); |
2028
|
|
|
} |
2029
|
|
|
} |
2030
|
|
|
|
2031
|
|
|
|
2032
|
|
|
// Undoes compensateScroll and restores all borders/margins |
2033
|
|
|
function uncompensateScroll(rowEls) { |
2034
|
|
|
rowEls.css({ |
2035
|
|
|
'margin-left': '', |
2036
|
|
|
'margin-right': '', |
2037
|
|
|
'border-left-width': '', |
2038
|
|
|
'border-right-width': '' |
2039
|
|
|
}); |
2040
|
|
|
} |
2041
|
|
|
|
2042
|
|
|
|
2043
|
|
|
// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate. |
2044
|
|
|
// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering |
2045
|
|
|
// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and |
2046
|
|
|
// reduces the available height. |
2047
|
|
|
function distributeHeight(els, availableHeight, shouldRedistribute) { |
2048
|
|
|
|
2049
|
|
|
// *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions, |
2050
|
|
|
// and it is better to be shorter than taller, to avoid creating unnecessary scrollbars. |
2051
|
|
|
|
2052
|
|
|
var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element |
2053
|
|
|
var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE* |
2054
|
|
|
var flexEls = []; // elements that are allowed to expand. array of DOM nodes |
2055
|
|
|
var flexOffsets = []; // amount of vertical space it takes up |
2056
|
|
|
var flexHeights = []; // actual css height |
2057
|
|
|
var usedHeight = 0; |
2058
|
|
|
|
2059
|
|
|
undistributeHeight(els); // give all elements their natural height |
2060
|
|
|
|
2061
|
|
|
// find elements that are below the recommended height (expandable). |
2062
|
|
|
// important to query for heights in a single first pass (to avoid reflow oscillation). |
2063
|
|
|
els.each(function(i, el) { |
2064
|
|
|
var minOffset = i === els.length - 1 ? minOffset2 : minOffset1; |
2065
|
|
|
var naturalOffset = $(el).outerHeight(true); |
2066
|
|
|
|
2067
|
|
|
if (naturalOffset < minOffset) { |
2068
|
|
|
flexEls.push(el); |
2069
|
|
|
flexOffsets.push(naturalOffset); |
2070
|
|
|
flexHeights.push($(el).height()); |
2071
|
|
|
} |
2072
|
|
|
else { |
2073
|
|
|
// this element stretches past recommended height (non-expandable). mark the space as occupied. |
2074
|
|
|
usedHeight += naturalOffset; |
2075
|
|
|
} |
2076
|
|
|
}); |
2077
|
|
|
|
2078
|
|
|
// readjust the recommended height to only consider the height available to non-maxed-out rows. |
2079
|
|
|
if (shouldRedistribute) { |
2080
|
|
|
availableHeight -= usedHeight; |
2081
|
|
|
minOffset1 = Math.floor(availableHeight / flexEls.length); |
2082
|
|
|
minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE* |
2083
|
|
|
} |
2084
|
|
|
|
2085
|
|
|
// assign heights to all expandable elements |
2086
|
|
|
$(flexEls).each(function(i, el) { |
2087
|
|
|
var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1; |
2088
|
|
|
var naturalOffset = flexOffsets[i]; |
2089
|
|
|
var naturalHeight = flexHeights[i]; |
2090
|
|
|
var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding |
2091
|
|
|
|
2092
|
|
|
if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things |
2093
|
|
|
$(el).height(newHeight); |
2094
|
|
|
} |
2095
|
|
|
}); |
2096
|
|
|
} |
2097
|
|
|
|
2098
|
|
|
|
2099
|
|
|
// Undoes distrubuteHeight, restoring all els to their natural height |
2100
|
|
|
function undistributeHeight(els) { |
2101
|
|
|
els.height(''); |
2102
|
|
|
} |
2103
|
|
|
|
2104
|
|
|
|
2105
|
|
|
// Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the |
2106
|
|
|
// cells to be that width. |
2107
|
|
|
// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline |
2108
|
|
|
function matchCellWidths(els) { |
2109
|
|
|
var maxInnerWidth = 0; |
2110
|
|
|
|
2111
|
|
|
els.find('> *').each(function(i, innerEl) { |
2112
|
|
|
var innerWidth = $(innerEl).outerWidth(); |
2113
|
|
|
if (innerWidth > maxInnerWidth) { |
2114
|
|
|
maxInnerWidth = innerWidth; |
2115
|
|
|
} |
2116
|
|
|
}); |
2117
|
|
|
|
2118
|
|
|
maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance |
2119
|
|
|
|
2120
|
|
|
els.width(maxInnerWidth); |
2121
|
|
|
|
2122
|
|
|
return maxInnerWidth; |
2123
|
|
|
} |
2124
|
|
|
|
2125
|
|
|
|
2126
|
|
|
// Turns a container element into a scroller if its contents is taller than the allotted height. |
2127
|
|
|
// Returns true if the element is now a scroller, false otherwise. |
2128
|
|
|
// NOTE: this method is best because it takes weird zooming dimensions into account |
2129
|
|
|
function setPotentialScroller(containerEl, height) { |
2130
|
|
|
containerEl.height(height).addClass('fc-scroller'); |
2131
|
|
|
|
2132
|
|
|
// are scrollbars needed? |
2133
|
|
|
if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :( |
2134
|
|
|
return true; |
2135
|
|
|
} |
2136
|
|
|
|
2137
|
|
|
unsetScroller(containerEl); // undo |
2138
|
|
|
return false; |
2139
|
|
|
} |
2140
|
|
|
|
2141
|
|
|
|
2142
|
|
|
// Takes an element that might have been a scroller, and turns it back into a normal element. |
2143
|
|
|
function unsetScroller(containerEl) { |
2144
|
|
|
containerEl.height('').removeClass('fc-scroller'); |
2145
|
|
|
} |
2146
|
|
|
|
2147
|
|
|
|
2148
|
|
|
/* General DOM Utilities |
2149
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
2150
|
|
|
|
2151
|
|
|
|
2152
|
|
|
// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51 |
2153
|
|
|
function getScrollParent(el) { |
2154
|
|
|
var position = el.css('position'), |
2155
|
|
|
scrollParent = el.parents().filter(function() { |
2156
|
|
|
var parent = $(this); |
2157
|
|
|
return (/(auto|scroll)/).test( |
2158
|
|
|
parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x') |
2159
|
|
|
); |
2160
|
|
|
}).eq(0); |
2161
|
|
|
|
2162
|
|
|
return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent; |
2163
|
|
|
} |
2164
|
|
|
|
2165
|
|
|
|
2166
|
|
|
// Given a container element, return an object with the pixel values of the left/right scrollbars. |
2167
|
|
|
// Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested. |
2168
|
|
|
// PREREQUISITE: container element must have a single child with display:block |
2169
|
|
|
function getScrollbarWidths(container) { |
2170
|
|
|
var containerLeft = container.offset().left; |
2171
|
|
|
var containerRight = containerLeft + container.width(); |
2172
|
|
|
var inner = container.children(); |
2173
|
|
|
var innerLeft = inner.offset().left; |
2174
|
|
|
var innerRight = innerLeft + inner.outerWidth(); |
2175
|
|
|
|
2176
|
|
|
return { |
2177
|
|
|
left: innerLeft - containerLeft, |
2178
|
|
|
right: containerRight - innerRight |
2179
|
|
|
}; |
2180
|
|
|
} |
2181
|
|
|
|
2182
|
|
|
|
2183
|
|
|
// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) |
2184
|
|
|
function isPrimaryMouseButton(ev) { |
2185
|
|
|
return ev.which == 1 && !ev.ctrlKey; |
2186
|
|
|
} |
2187
|
|
|
|
2188
|
|
|
|
2189
|
|
|
/* FullCalendar-specific Misc Utilities |
2190
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
2191
|
|
|
|
2192
|
|
|
|
2193
|
|
|
// Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection. |
2194
|
|
|
// Expects all dates to be normalized to the same timezone beforehand. |
2195
|
|
|
function intersectionToSeg(subjectStart, subjectEnd, intervalStart, intervalEnd) { |
2196
|
|
|
var segStart, segEnd; |
2197
|
|
|
var isStart, isEnd; |
2198
|
|
|
|
2199
|
|
|
if (subjectEnd > intervalStart && subjectStart < intervalEnd) { // in bounds at all? |
2200
|
|
|
|
2201
|
|
|
if (subjectStart >= intervalStart) { |
2202
|
|
|
segStart = subjectStart.clone(); |
2203
|
|
|
isStart = true; |
2204
|
|
|
} |
2205
|
|
|
else { |
2206
|
|
|
segStart = intervalStart.clone(); |
2207
|
|
|
isStart = false; |
2208
|
|
|
} |
2209
|
|
|
|
2210
|
|
|
if (subjectEnd <= intervalEnd) { |
2211
|
|
|
segEnd = subjectEnd.clone(); |
2212
|
|
|
isEnd = true; |
2213
|
|
|
} |
2214
|
|
|
else { |
2215
|
|
|
segEnd = intervalEnd.clone(); |
2216
|
|
|
isEnd = false; |
2217
|
|
|
} |
2218
|
|
|
|
2219
|
|
|
return { |
2220
|
|
|
start: segStart, |
2221
|
|
|
end: segEnd, |
2222
|
|
|
isStart: isStart, |
2223
|
|
|
isEnd: isEnd |
2224
|
|
|
}; |
2225
|
|
|
} |
2226
|
|
|
} |
2227
|
|
|
|
2228
|
|
|
|
2229
|
|
|
function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object |
2230
|
|
|
obj = obj || {}; |
2231
|
|
|
if (obj[name] !== undefined) { |
2232
|
|
|
return obj[name]; |
2233
|
|
|
} |
2234
|
|
|
var parts = name.split(/(?=[A-Z])/), |
2235
|
|
|
i = parts.length - 1, res; |
2236
|
|
|
for (; i>=0; i--) { |
2237
|
|
|
res = obj[parts[i].toLowerCase()]; |
2238
|
|
|
if (res !== undefined) { |
2239
|
|
|
return res; |
2240
|
|
|
} |
2241
|
|
|
} |
2242
|
|
|
return obj['default']; |
2243
|
|
|
} |
2244
|
|
|
|
2245
|
|
|
|
2246
|
|
|
/* Date Utilities |
2247
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
2248
|
|
|
|
2249
|
|
|
var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]; |
2250
|
|
|
|
2251
|
|
|
|
2252
|
|
|
// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time. |
2253
|
|
|
// Moments will have their timezones normalized. |
2254
|
|
|
function dayishDiff(a, b) { |
2255
|
|
|
return moment.duration({ |
2256
|
|
|
days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'), |
2257
|
|
|
ms: a.time() - b.time() |
2258
|
|
|
}); |
2259
|
|
|
} |
2260
|
|
|
|
2261
|
|
|
|
2262
|
|
|
function isNativeDate(input) { |
2263
|
|
|
return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date; |
2264
|
|
|
} |
2265
|
|
|
|
2266
|
|
|
|
2267
|
|
|
function dateCompare(a, b) { // works with Moments and native Dates |
2268
|
|
|
return a - b; |
2269
|
|
|
} |
2270
|
|
|
|
2271
|
|
|
|
2272
|
|
|
/* General Utilities |
2273
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
2274
|
|
|
|
2275
|
|
|
fc.applyAll = applyAll; // export |
2276
|
|
|
|
2277
|
|
|
|
2278
|
|
|
// Create an object that has the given prototype. Just like Object.create |
2279
|
|
|
function createObject(proto) { |
2280
|
|
|
var f = function() {}; |
2281
|
|
|
f.prototype = proto; |
2282
|
|
|
return new f(); |
|
|
|
|
2283
|
|
|
} |
2284
|
|
|
|
2285
|
|
|
|
2286
|
|
|
// Copies specifically-owned (non-protoype) properties of `b` onto `a`. |
2287
|
|
|
// FYI, $.extend would copy *all* properties of `b` onto `a`. |
2288
|
|
|
function extend(a, b) { |
2289
|
|
|
for (var i in b) { |
2290
|
|
|
if (b.hasOwnProperty(i)) { |
2291
|
|
|
a[i] = b[i]; |
2292
|
|
|
} |
2293
|
|
|
} |
2294
|
|
|
} |
2295
|
|
|
|
2296
|
|
|
|
2297
|
|
|
function applyAll(functions, thisObj, args) { |
2298
|
|
|
if ($.isFunction(functions)) { |
2299
|
|
|
functions = [ functions ]; |
2300
|
|
|
} |
2301
|
|
|
if (functions) { |
2302
|
|
|
var i; |
2303
|
|
|
var ret; |
2304
|
|
|
for (i=0; i<functions.length; i++) { |
2305
|
|
|
ret = functions[i].apply(thisObj, args) || ret; |
2306
|
|
|
} |
2307
|
|
|
return ret; |
|
|
|
|
2308
|
|
|
} |
2309
|
|
|
} |
2310
|
|
|
|
2311
|
|
|
|
2312
|
|
|
function firstDefined() { |
2313
|
|
|
for (var i=0; i<arguments.length; i++) { |
2314
|
|
|
if (arguments[i] !== undefined) { |
2315
|
|
|
return arguments[i]; |
2316
|
|
|
} |
2317
|
|
|
} |
|
|
|
|
2318
|
|
|
} |
2319
|
|
|
|
2320
|
|
|
|
2321
|
|
|
function htmlEscape(s) { |
2322
|
|
|
return (s + '').replace(/&/g, '&') |
2323
|
|
|
.replace(/</g, '<') |
2324
|
|
|
.replace(/>/g, '>') |
2325
|
|
|
.replace(/'/g, ''') |
2326
|
|
|
.replace(/"/g, '"') |
2327
|
|
|
.replace(/\n/g, '<br />'); |
2328
|
|
|
} |
2329
|
|
|
|
2330
|
|
|
|
2331
|
|
|
function stripHtmlEntities(text) { |
2332
|
|
|
return text.replace(/&.*?;/g, ''); |
2333
|
|
|
} |
2334
|
|
|
|
2335
|
|
|
|
2336
|
|
|
function capitaliseFirstLetter(str) { |
2337
|
|
|
return str.charAt(0).toUpperCase() + str.slice(1); |
2338
|
|
|
} |
2339
|
|
|
|
2340
|
|
|
|
2341
|
|
|
// Returns a function, that, as long as it continues to be invoked, will not |
2342
|
|
|
// be triggered. The function will be called after it stops being called for |
2343
|
|
|
// N milliseconds. |
2344
|
|
|
// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714 |
2345
|
|
|
function debounce(func, wait) { |
2346
|
|
|
var timeoutId; |
2347
|
|
|
var args; |
2348
|
|
|
var context; |
2349
|
|
|
var timestamp; // of most recent call |
2350
|
|
|
var later = function() { |
2351
|
|
|
var last = +new Date() - timestamp; |
2352
|
|
|
if (last < wait && last > 0) { |
2353
|
|
|
timeoutId = setTimeout(later, wait - last); |
2354
|
|
|
} |
2355
|
|
|
else { |
2356
|
|
|
timeoutId = null; |
2357
|
|
|
func.apply(context, args); |
2358
|
|
|
if (!timeoutId) { |
2359
|
|
|
context = args = null; |
2360
|
|
|
} |
2361
|
|
|
} |
2362
|
|
|
}; |
2363
|
|
|
|
2364
|
|
|
return function() { |
2365
|
|
|
context = this; |
2366
|
|
|
args = arguments; |
2367
|
|
|
timestamp = +new Date(); |
2368
|
|
|
if (!timeoutId) { |
2369
|
|
|
timeoutId = setTimeout(later, wait); |
2370
|
|
|
} |
2371
|
|
|
}; |
2372
|
|
|
} |
2373
|
|
|
|
2374
|
|
|
;; |
2375
|
|
|
|
2376
|
|
|
var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/; |
2377
|
|
|
var ambigTimeOrZoneRegex = |
2378
|
|
|
/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/; |
2379
|
|
|
|
2380
|
|
|
|
2381
|
|
|
// Creating |
2382
|
|
|
// ------------------------------------------------------------------------------------------------- |
2383
|
|
|
|
2384
|
|
|
// Creates a new moment, similar to the vanilla moment(...) constructor, but with |
2385
|
|
|
// extra features (ambiguous time, enhanced formatting). When gived an existing moment, |
2386
|
|
|
// it will function as a clone (and retain the zone of the moment). Anything else will |
2387
|
|
|
// result in a moment in the local zone. |
2388
|
|
|
fc.moment = function() { |
2389
|
|
|
return makeMoment(arguments); |
2390
|
|
|
}; |
2391
|
|
|
|
2392
|
|
|
// Sames as fc.moment, but forces the resulting moment to be in the UTC timezone. |
2393
|
|
|
fc.moment.utc = function() { |
2394
|
|
|
var mom = makeMoment(arguments, true); |
2395
|
|
|
|
2396
|
|
|
// Force it into UTC because makeMoment doesn't guarantee it. |
2397
|
|
|
if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone |
2398
|
|
|
mom.utc(); |
2399
|
|
|
} |
2400
|
|
|
|
2401
|
|
|
return mom; |
2402
|
|
|
}; |
2403
|
|
|
|
2404
|
|
|
// Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved. |
2405
|
|
|
// ISO8601 strings with no timezone offset will become ambiguously zoned. |
2406
|
|
|
fc.moment.parseZone = function() { |
2407
|
|
|
return makeMoment(arguments, true, true); |
2408
|
|
|
}; |
2409
|
|
|
|
2410
|
|
|
// Builds an FCMoment from args. When given an existing moment, it clones. When given a native |
2411
|
|
|
// Date, or called with no arguments (the current time), the resulting moment will be local. |
2412
|
|
|
// Anything else needs to be "parsed" (a string or an array), and will be affected by: |
2413
|
|
|
// parseAsUTC - if there is no zone information, should we parse the input in UTC? |
2414
|
|
|
// parseZone - if there is zone information, should we force the zone of the moment? |
2415
|
|
|
function makeMoment(args, parseAsUTC, parseZone) { |
2416
|
|
|
var input = args[0]; |
2417
|
|
|
var isSingleString = args.length == 1 && typeof input === 'string'; |
2418
|
|
|
var isAmbigTime; |
2419
|
|
|
var isAmbigZone; |
2420
|
|
|
var ambigMatch; |
2421
|
|
|
var output; // an object with fields for the new FCMoment object |
2422
|
|
|
|
2423
|
|
|
if (moment.isMoment(input)) { |
2424
|
|
|
output = moment.apply(null, args); // clone it |
2425
|
|
|
|
2426
|
|
|
// the ambig properties have not been preserved in the clone, so reassign them |
2427
|
|
|
if (input._ambigTime) { |
2428
|
|
|
output._ambigTime = true; |
2429
|
|
|
} |
2430
|
|
|
if (input._ambigZone) { |
2431
|
|
|
output._ambigZone = true; |
2432
|
|
|
} |
2433
|
|
|
} |
2434
|
|
|
else if (isNativeDate(input) || input === undefined) { |
2435
|
|
|
output = moment.apply(null, args); // will be local |
2436
|
|
|
} |
2437
|
|
|
else { // "parsing" is required |
2438
|
|
|
isAmbigTime = false; |
2439
|
|
|
isAmbigZone = false; |
2440
|
|
|
|
2441
|
|
|
if (isSingleString) { |
2442
|
|
|
if (ambigDateOfMonthRegex.test(input)) { |
2443
|
|
|
// accept strings like '2014-05', but convert to the first of the month |
2444
|
|
|
input += '-01'; |
2445
|
|
|
args = [ input ]; // for when we pass it on to moment's constructor |
2446
|
|
|
isAmbigTime = true; |
2447
|
|
|
isAmbigZone = true; |
2448
|
|
|
} |
2449
|
|
|
else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) { |
2450
|
|
|
isAmbigTime = !ambigMatch[5]; // no time part? |
2451
|
|
|
isAmbigZone = true; |
2452
|
|
|
} |
2453
|
|
|
} |
2454
|
|
|
else if ($.isArray(input)) { |
2455
|
|
|
// arrays have no timezone information, so assume ambiguous zone |
2456
|
|
|
isAmbigZone = true; |
2457
|
|
|
} |
2458
|
|
|
// otherwise, probably a string with a format |
2459
|
|
|
|
2460
|
|
|
if (parseAsUTC) { |
2461
|
|
|
output = moment.utc.apply(moment, args); |
2462
|
|
|
} |
2463
|
|
|
else { |
2464
|
|
|
output = moment.apply(null, args); |
2465
|
|
|
} |
2466
|
|
|
|
2467
|
|
|
if (isAmbigTime) { |
2468
|
|
|
output._ambigTime = true; |
2469
|
|
|
output._ambigZone = true; // ambiguous time always means ambiguous zone |
2470
|
|
|
} |
2471
|
|
|
else if (parseZone) { // let's record the inputted zone somehow |
2472
|
|
|
if (isAmbigZone) { |
2473
|
|
|
output._ambigZone = true; |
2474
|
|
|
} |
2475
|
|
|
else if (isSingleString) { |
2476
|
|
|
output.zone(input); // if not a valid zone, will assign UTC |
2477
|
|
|
} |
2478
|
|
|
} |
2479
|
|
|
} |
2480
|
|
|
|
2481
|
|
|
return new FCMoment(output); |
2482
|
|
|
} |
2483
|
|
|
|
2484
|
|
|
// Our subclass of Moment. |
2485
|
|
|
// Accepts an object with the internal Moment properties that should be copied over to |
2486
|
|
|
// `this` object (most likely another Moment object). The values in this data must not |
2487
|
|
|
// be referenced by anything else (two moments sharing a Date object for example). |
2488
|
|
|
function FCMoment(internalData) { |
2489
|
|
|
extend(this, internalData); |
2490
|
|
|
} |
2491
|
|
|
|
2492
|
|
|
// Chain the prototype to Moment's |
2493
|
|
|
FCMoment.prototype = createObject(moment.fn); |
2494
|
|
|
|
2495
|
|
|
// We need this because Moment's implementation won't create an FCMoment, |
2496
|
|
|
// nor will it copy over the ambig flags. |
2497
|
|
|
FCMoment.prototype.clone = function() { |
2498
|
|
|
return makeMoment([ this ]); |
2499
|
|
|
}; |
2500
|
|
|
|
2501
|
|
|
|
2502
|
|
|
// Time-of-day |
2503
|
|
|
// ------------------------------------------------------------------------------------------------- |
2504
|
|
|
|
2505
|
|
|
// GETTER |
2506
|
|
|
// Returns a Duration with the hours/minutes/seconds/ms values of the moment. |
2507
|
|
|
// If the moment has an ambiguous time, a duration of 00:00 will be returned. |
2508
|
|
|
// |
2509
|
|
|
// SETTER |
2510
|
|
|
// You can supply a Duration, a Moment, or a Duration-like argument. |
2511
|
|
|
// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous. |
2512
|
|
|
FCMoment.prototype.time = function(time) { |
2513
|
|
|
if (time == null) { // getter |
2514
|
|
|
return moment.duration({ |
2515
|
|
|
hours: this.hours(), |
2516
|
|
|
minutes: this.minutes(), |
2517
|
|
|
seconds: this.seconds(), |
2518
|
|
|
milliseconds: this.milliseconds() |
2519
|
|
|
}); |
2520
|
|
|
} |
2521
|
|
|
else { // setter |
2522
|
|
|
|
2523
|
|
|
delete this._ambigTime; // mark that the moment now has a time |
2524
|
|
|
|
2525
|
|
|
if (!moment.isDuration(time) && !moment.isMoment(time)) { |
2526
|
|
|
time = moment.duration(time); |
2527
|
|
|
} |
2528
|
|
|
|
2529
|
|
|
// The day value should cause overflow (so 24 hours becomes 00:00:00 of next day). |
2530
|
|
|
// Only for Duration times, not Moment times. |
2531
|
|
|
var dayHours = 0; |
2532
|
|
|
if (moment.isDuration(time)) { |
2533
|
|
|
dayHours = Math.floor(time.asDays()) * 24; |
2534
|
|
|
} |
2535
|
|
|
|
2536
|
|
|
// We need to set the individual fields. |
2537
|
|
|
// Can't use startOf('day') then add duration. In case of DST at start of day. |
2538
|
|
|
return this.hours(dayHours + time.hours()) |
2539
|
|
|
.minutes(time.minutes()) |
2540
|
|
|
.seconds(time.seconds()) |
2541
|
|
|
.milliseconds(time.milliseconds()); |
2542
|
|
|
} |
2543
|
|
|
}; |
2544
|
|
|
|
2545
|
|
|
// Converts the moment to UTC, stripping out its time-of-day and timezone offset, |
2546
|
|
|
// but preserving its YMD. A moment with a stripped time will display no time |
2547
|
|
|
// nor timezone offset when .format() is called. |
2548
|
|
|
FCMoment.prototype.stripTime = function() { |
2549
|
|
|
var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array |
2550
|
|
|
|
2551
|
|
|
// set the internal UTC flag |
2552
|
|
|
moment.fn.utc.call(this); // call the original method, because we don't want to affect _ambigZone |
2553
|
|
|
|
2554
|
|
|
this.year(a[0]) // TODO: find a way to do this in one shot |
2555
|
|
|
.month(a[1]) |
2556
|
|
|
.date(a[2]) |
2557
|
|
|
.hours(0) |
2558
|
|
|
.minutes(0) |
2559
|
|
|
.seconds(0) |
2560
|
|
|
.milliseconds(0); |
2561
|
|
|
|
2562
|
|
|
// Mark the time as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which |
2563
|
|
|
// clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone. |
2564
|
|
|
this._ambigTime = true; |
2565
|
|
|
this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset |
2566
|
|
|
|
2567
|
|
|
return this; // for chaining |
2568
|
|
|
}; |
2569
|
|
|
|
2570
|
|
|
// Returns if the moment has a non-ambiguous time (boolean) |
2571
|
|
|
FCMoment.prototype.hasTime = function() { |
2572
|
|
|
return !this._ambigTime; |
2573
|
|
|
}; |
2574
|
|
|
|
2575
|
|
|
|
2576
|
|
|
// Timezone |
2577
|
|
|
// ------------------------------------------------------------------------------------------------- |
2578
|
|
|
|
2579
|
|
|
// Converts the moment to UTC, stripping out its timezone offset, but preserving its |
2580
|
|
|
// YMD and time-of-day. A moment with a stripped timezone offset will display no |
2581
|
|
|
// timezone offset when .format() is called. |
2582
|
|
|
FCMoment.prototype.stripZone = function() { |
2583
|
|
|
var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array |
2584
|
|
|
var wasAmbigTime = this._ambigTime; |
2585
|
|
|
|
2586
|
|
|
moment.fn.utc.call(this); // set the internal UTC flag |
2587
|
|
|
|
2588
|
|
|
this.year(a[0]) // TODO: find a way to do this in one shot |
2589
|
|
|
.month(a[1]) |
2590
|
|
|
.date(a[2]) |
2591
|
|
|
.hours(a[3]) |
2592
|
|
|
.minutes(a[4]) |
2593
|
|
|
.seconds(a[5]) |
2594
|
|
|
.milliseconds(a[6]); |
2595
|
|
|
|
2596
|
|
|
if (wasAmbigTime) { |
2597
|
|
|
// the above call to .utc()/.zone() unfortunately clears the ambig flags, so reassign |
2598
|
|
|
this._ambigTime = true; |
2599
|
|
|
} |
2600
|
|
|
|
2601
|
|
|
// Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which |
2602
|
|
|
// clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone. |
2603
|
|
|
this._ambigZone = true; |
2604
|
|
|
|
2605
|
|
|
return this; // for chaining |
2606
|
|
|
}; |
2607
|
|
|
|
2608
|
|
|
// Returns of the moment has a non-ambiguous timezone offset (boolean) |
2609
|
|
|
FCMoment.prototype.hasZone = function() { |
2610
|
|
|
return !this._ambigZone; |
2611
|
|
|
}; |
2612
|
|
|
|
2613
|
|
|
// this method implicitly marks a zone |
2614
|
|
|
FCMoment.prototype.zone = function(tzo) { |
2615
|
|
|
|
2616
|
|
|
if (tzo != null) { |
2617
|
|
|
// FYI, the delete statements need to be before the .zone() call or else chaos ensues |
2618
|
|
|
// for reasons I don't understand. |
2619
|
|
|
delete this._ambigTime; |
2620
|
|
|
delete this._ambigZone; |
2621
|
|
|
} |
2622
|
|
|
|
2623
|
|
|
return moment.fn.zone.apply(this, arguments); |
2624
|
|
|
}; |
2625
|
|
|
|
2626
|
|
|
// this method implicitly marks a zone |
2627
|
|
|
FCMoment.prototype.local = function() { |
2628
|
|
|
var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array |
2629
|
|
|
var wasAmbigZone = this._ambigZone; |
2630
|
|
|
|
2631
|
|
|
// will happen anyway via .local()/.zone(), but don't want to rely on internal implementation |
2632
|
|
|
delete this._ambigTime; |
2633
|
|
|
delete this._ambigZone; |
2634
|
|
|
|
2635
|
|
|
moment.fn.local.apply(this, arguments); |
2636
|
|
|
|
2637
|
|
|
if (wasAmbigZone) { |
2638
|
|
|
// If the moment was ambiguously zoned, the date fields were stored as UTC. |
2639
|
|
|
// We want to preserve these, but in local time. |
2640
|
|
|
this.year(a[0]) // TODO: find a way to do this in one shot |
2641
|
|
|
.month(a[1]) |
2642
|
|
|
.date(a[2]) |
2643
|
|
|
.hours(a[3]) |
2644
|
|
|
.minutes(a[4]) |
2645
|
|
|
.seconds(a[5]) |
2646
|
|
|
.milliseconds(a[6]); |
2647
|
|
|
} |
2648
|
|
|
|
2649
|
|
|
return this; // for chaining |
2650
|
|
|
}; |
2651
|
|
|
|
2652
|
|
|
// this method implicitly marks a zone |
2653
|
|
|
FCMoment.prototype.utc = function() { |
2654
|
|
|
|
2655
|
|
|
// will happen anyway via .local()/.zone(), but don't want to rely on internal implementation |
2656
|
|
|
delete this._ambigTime; |
2657
|
|
|
delete this._ambigZone; |
2658
|
|
|
|
2659
|
|
|
return moment.fn.utc.apply(this, arguments); |
2660
|
|
|
}; |
2661
|
|
|
|
2662
|
|
|
|
2663
|
|
|
// Formatting |
2664
|
|
|
// ------------------------------------------------------------------------------------------------- |
2665
|
|
|
|
2666
|
|
|
FCMoment.prototype.format = function() { |
2667
|
|
|
if (arguments[0]) { |
2668
|
|
|
return formatDate(this, arguments[0]); // our extended formatting |
2669
|
|
|
} |
2670
|
|
|
if (this._ambigTime) { |
2671
|
|
|
return momentFormat(this, 'YYYY-MM-DD'); |
2672
|
|
|
} |
2673
|
|
|
if (this._ambigZone) { |
2674
|
|
|
return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); |
2675
|
|
|
} |
2676
|
|
|
return momentFormat(this); // default moment original formatting |
2677
|
|
|
}; |
2678
|
|
|
|
2679
|
|
|
FCMoment.prototype.toISOString = function() { |
2680
|
|
|
if (this._ambigTime) { |
2681
|
|
|
return momentFormat(this, 'YYYY-MM-DD'); |
2682
|
|
|
} |
2683
|
|
|
if (this._ambigZone) { |
2684
|
|
|
return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); |
2685
|
|
|
} |
2686
|
|
|
return moment.fn.toISOString.apply(this, arguments); |
2687
|
|
|
}; |
2688
|
|
|
|
2689
|
|
|
|
2690
|
|
|
// Querying |
2691
|
|
|
// ------------------------------------------------------------------------------------------------- |
2692
|
|
|
|
2693
|
|
|
// Is the moment within the specified range? `end` is exclusive. |
2694
|
|
|
FCMoment.prototype.isWithin = function(start, end) { |
2695
|
|
|
var a = commonlyAmbiguate([ this, start, end ]); |
2696
|
|
|
return a[0] >= a[1] && a[0] < a[2]; |
2697
|
|
|
}; |
2698
|
|
|
|
2699
|
|
|
// When isSame is called with units, timezone ambiguity is normalized before the comparison happens. |
2700
|
|
|
// If no units are specified, the two moments must be identically the same, with matching ambig flags. |
2701
|
|
|
FCMoment.prototype.isSame = function(input, units) { |
2702
|
|
|
var a; |
2703
|
|
|
|
2704
|
|
|
if (units) { |
2705
|
|
|
a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times |
2706
|
|
|
return moment.fn.isSame.call(a[0], a[1], units); |
2707
|
|
|
} |
2708
|
|
|
else { |
2709
|
|
|
input = fc.moment.parseZone(input); // normalize input |
2710
|
|
|
return moment.fn.isSame.call(this, input) && |
2711
|
|
|
Boolean(this._ambigTime) === Boolean(input._ambigTime) && |
2712
|
|
|
Boolean(this._ambigZone) === Boolean(input._ambigZone); |
2713
|
|
|
} |
2714
|
|
|
}; |
2715
|
|
|
|
2716
|
|
|
// Make these query methods work with ambiguous moments |
2717
|
|
|
$.each([ |
2718
|
|
|
'isBefore', |
2719
|
|
|
'isAfter' |
2720
|
|
|
], function(i, methodName) { |
2721
|
|
|
FCMoment.prototype[methodName] = function(input, units) { |
2722
|
|
|
var a = commonlyAmbiguate([ this, input ]); |
2723
|
|
|
return moment.fn[methodName].call(a[0], a[1], units); |
2724
|
|
|
}; |
2725
|
|
|
}); |
2726
|
|
|
|
2727
|
|
|
|
2728
|
|
|
// Misc Internals |
2729
|
|
|
// ------------------------------------------------------------------------------------------------- |
2730
|
|
|
|
2731
|
|
|
// given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated. |
2732
|
|
|
// for example, of one moment has ambig time, but not others, all moments will have their time stripped. |
2733
|
|
|
// set `preserveTime` to `true` to keep times, but only normalize zone ambiguity. |
2734
|
|
|
function commonlyAmbiguate(inputs, preserveTime) { |
2735
|
|
|
var outputs = []; |
2736
|
|
|
var anyAmbigTime = false; |
2737
|
|
|
var anyAmbigZone = false; |
2738
|
|
|
var i; |
2739
|
|
|
|
2740
|
|
|
for (i=0; i<inputs.length; i++) { |
2741
|
|
|
outputs.push(fc.moment.parseZone(inputs[i])); |
2742
|
|
|
anyAmbigTime = anyAmbigTime || outputs[i]._ambigTime; |
2743
|
|
|
anyAmbigZone = anyAmbigZone || outputs[i]._ambigZone; |
2744
|
|
|
} |
2745
|
|
|
|
2746
|
|
|
for (i=0; i<outputs.length; i++) { |
2747
|
|
|
if (anyAmbigTime && !preserveTime) { |
2748
|
|
|
outputs[i].stripTime(); |
2749
|
|
|
} |
2750
|
|
|
else if (anyAmbigZone) { |
2751
|
|
|
outputs[i].stripZone(); |
2752
|
|
|
} |
2753
|
|
|
} |
2754
|
|
|
|
2755
|
|
|
return outputs; |
2756
|
|
|
} |
2757
|
|
|
|
2758
|
|
|
;; |
2759
|
|
|
|
2760
|
|
|
// Single Date Formatting |
2761
|
|
|
// ------------------------------------------------------------------------------------------------- |
2762
|
|
|
|
2763
|
|
|
|
2764
|
|
|
// call this if you want Moment's original format method to be used |
2765
|
|
|
function momentFormat(mom, formatStr) { |
2766
|
|
|
return moment.fn.format.call(mom, formatStr); |
2767
|
|
|
} |
2768
|
|
|
|
2769
|
|
|
|
2770
|
|
|
// Formats `date` with a Moment formatting string, but allow our non-zero areas and |
2771
|
|
|
// additional token. |
2772
|
|
|
function formatDate(date, formatStr) { |
2773
|
|
|
return formatDateWithChunks(date, getFormatStringChunks(formatStr)); |
2774
|
|
|
} |
2775
|
|
|
|
2776
|
|
|
|
2777
|
|
|
function formatDateWithChunks(date, chunks) { |
2778
|
|
|
var s = ''; |
2779
|
|
|
var i; |
2780
|
|
|
|
2781
|
|
|
for (i=0; i<chunks.length; i++) { |
2782
|
|
|
s += formatDateWithChunk(date, chunks[i]); |
2783
|
|
|
} |
2784
|
|
|
|
2785
|
|
|
return s; |
2786
|
|
|
} |
2787
|
|
|
|
2788
|
|
|
|
2789
|
|
|
// addition formatting tokens we want recognized |
2790
|
|
|
var tokenOverrides = { |
2791
|
|
|
t: function(date) { // "a" or "p" |
2792
|
|
|
return momentFormat(date, 'a').charAt(0); |
2793
|
|
|
}, |
2794
|
|
|
T: function(date) { // "A" or "P" |
2795
|
|
|
return momentFormat(date, 'A').charAt(0); |
2796
|
|
|
} |
2797
|
|
|
}; |
2798
|
|
|
|
2799
|
|
|
|
2800
|
|
|
function formatDateWithChunk(date, chunk) { |
2801
|
|
|
var token; |
2802
|
|
|
var maybeStr; |
2803
|
|
|
|
2804
|
|
|
if (typeof chunk === 'string') { // a literal string |
2805
|
|
|
return chunk; |
2806
|
|
|
} |
2807
|
|
|
else if ((token = chunk.token)) { // a token, like "YYYY" |
2808
|
|
|
if (tokenOverrides[token]) { |
2809
|
|
|
return tokenOverrides[token](date); // use our custom token |
2810
|
|
|
} |
2811
|
|
|
return momentFormat(date, token); |
2812
|
|
|
} |
2813
|
|
|
else if (chunk.maybe) { // a grouping of other chunks that must be non-zero |
2814
|
|
|
maybeStr = formatDateWithChunks(date, chunk.maybe); |
2815
|
|
|
if (maybeStr.match(/[1-9]/)) { |
2816
|
|
|
return maybeStr; |
2817
|
|
|
} |
2818
|
|
|
} |
2819
|
|
|
|
2820
|
|
|
return ''; |
2821
|
|
|
} |
2822
|
|
|
|
2823
|
|
|
|
2824
|
|
|
// Date Range Formatting |
2825
|
|
|
// ------------------------------------------------------------------------------------------------- |
2826
|
|
|
// TODO: make it work with timezone offset |
2827
|
|
|
|
2828
|
|
|
// Using a formatting string meant for a single date, generate a range string, like |
2829
|
|
|
// "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ. |
2830
|
|
|
// If the dates are the same as far as the format string is concerned, just return a single |
2831
|
|
|
// rendering of one date, without any separator. |
2832
|
|
|
function formatRange(date1, date2, formatStr, separator, isRTL) { |
2833
|
|
|
var localeData; |
2834
|
|
|
|
2835
|
|
|
date1 = fc.moment.parseZone(date1); |
2836
|
|
|
date2 = fc.moment.parseZone(date2); |
2837
|
|
|
|
2838
|
|
|
localeData = (date1.localeData || date1.lang).call(date1); // works with moment-pre-2.8 |
2839
|
|
|
|
2840
|
|
|
// Expand localized format strings, like "LL" -> "MMMM D YYYY" |
2841
|
|
|
formatStr = localeData.longDateFormat(formatStr) || formatStr; |
2842
|
|
|
// BTW, this is not important for `formatDate` because it is impossible to put custom tokens |
2843
|
|
|
// or non-zero areas in Moment's localized format strings. |
2844
|
|
|
|
2845
|
|
|
separator = separator || ' - '; |
2846
|
|
|
|
2847
|
|
|
return formatRangeWithChunks( |
2848
|
|
|
date1, |
2849
|
|
|
date2, |
2850
|
|
|
getFormatStringChunks(formatStr), |
2851
|
|
|
separator, |
2852
|
|
|
isRTL |
2853
|
|
|
); |
2854
|
|
|
} |
2855
|
|
|
fc.formatRange = formatRange; // expose |
2856
|
|
|
|
2857
|
|
|
|
2858
|
|
|
function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) { |
2859
|
|
|
var chunkStr; // the rendering of the chunk |
2860
|
|
|
var leftI; |
2861
|
|
|
var leftStr = ''; |
2862
|
|
|
var rightI; |
2863
|
|
|
var rightStr = ''; |
2864
|
|
|
var middleI; |
2865
|
|
|
var middleStr1 = ''; |
2866
|
|
|
var middleStr2 = ''; |
2867
|
|
|
var middleStr = ''; |
2868
|
|
|
|
2869
|
|
|
// Start at the leftmost side of the formatting string and continue until you hit a token |
2870
|
|
|
// that is not the same between dates. |
2871
|
|
|
for (leftI=0; leftI<chunks.length; leftI++) { |
2872
|
|
|
chunkStr = formatSimilarChunk(date1, date2, chunks[leftI]); |
2873
|
|
|
if (chunkStr === false) { |
2874
|
|
|
break; |
2875
|
|
|
} |
2876
|
|
|
leftStr += chunkStr; |
2877
|
|
|
} |
2878
|
|
|
|
2879
|
|
|
// Similarly, start at the rightmost side of the formatting string and move left |
2880
|
|
|
for (rightI=chunks.length-1; rightI>leftI; rightI--) { |
2881
|
|
|
chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]); |
2882
|
|
|
if (chunkStr === false) { |
2883
|
|
|
break; |
2884
|
|
|
} |
2885
|
|
|
rightStr = chunkStr + rightStr; |
2886
|
|
|
} |
2887
|
|
|
|
2888
|
|
|
// The area in the middle is different for both of the dates. |
2889
|
|
|
// Collect them distinctly so we can jam them together later. |
2890
|
|
|
for (middleI=leftI; middleI<=rightI; middleI++) { |
2891
|
|
|
middleStr1 += formatDateWithChunk(date1, chunks[middleI]); |
2892
|
|
|
middleStr2 += formatDateWithChunk(date2, chunks[middleI]); |
2893
|
|
|
} |
2894
|
|
|
|
2895
|
|
|
if (middleStr1 || middleStr2) { |
2896
|
|
|
if (isRTL) { |
2897
|
|
|
middleStr = middleStr2 + separator + middleStr1; |
2898
|
|
|
} |
2899
|
|
|
else { |
2900
|
|
|
middleStr = middleStr1 + separator + middleStr2; |
2901
|
|
|
} |
2902
|
|
|
} |
2903
|
|
|
|
2904
|
|
|
return leftStr + middleStr + rightStr; |
2905
|
|
|
} |
2906
|
|
|
|
2907
|
|
|
|
2908
|
|
|
var similarUnitMap = { |
2909
|
|
|
Y: 'year', |
2910
|
|
|
M: 'month', |
2911
|
|
|
D: 'day', // day of month |
2912
|
|
|
d: 'day', // day of week |
2913
|
|
|
// prevents a separator between anything time-related... |
2914
|
|
|
A: 'second', // AM/PM |
2915
|
|
|
a: 'second', // am/pm |
2916
|
|
|
T: 'second', // A/P |
2917
|
|
|
t: 'second', // a/p |
2918
|
|
|
H: 'second', // hour (24) |
2919
|
|
|
h: 'second', // hour (12) |
2920
|
|
|
m: 'second', // minute |
2921
|
|
|
s: 'second' // second |
2922
|
|
|
}; |
2923
|
|
|
// TODO: week maybe? |
2924
|
|
|
|
2925
|
|
|
|
2926
|
|
|
// Given a formatting chunk, and given that both dates are similar in the regard the |
2927
|
|
|
// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`. |
2928
|
|
|
function formatSimilarChunk(date1, date2, chunk) { |
2929
|
|
|
var token; |
2930
|
|
|
var unit; |
2931
|
|
|
|
2932
|
|
|
if (typeof chunk === 'string') { // a literal string |
2933
|
|
|
return chunk; |
2934
|
|
|
} |
2935
|
|
|
else if ((token = chunk.token)) { |
2936
|
|
|
unit = similarUnitMap[token.charAt(0)]; |
2937
|
|
|
// are the dates the same for this unit of measurement? |
2938
|
|
|
if (unit && date1.isSame(date2, unit)) { |
2939
|
|
|
return momentFormat(date1, token); // would be the same if we used `date2` |
2940
|
|
|
// BTW, don't support custom tokens |
2941
|
|
|
} |
2942
|
|
|
} |
2943
|
|
|
|
2944
|
|
|
return false; // the chunk is NOT the same for the two dates |
2945
|
|
|
// BTW, don't support splitting on non-zero areas |
2946
|
|
|
} |
2947
|
|
|
|
2948
|
|
|
|
2949
|
|
|
// Chunking Utils |
2950
|
|
|
// ------------------------------------------------------------------------------------------------- |
2951
|
|
|
|
2952
|
|
|
|
2953
|
|
|
var formatStringChunkCache = {}; |
2954
|
|
|
|
2955
|
|
|
|
2956
|
|
|
function getFormatStringChunks(formatStr) { |
2957
|
|
|
if (formatStr in formatStringChunkCache) { |
2958
|
|
|
return formatStringChunkCache[formatStr]; |
2959
|
|
|
} |
2960
|
|
|
return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr)); |
2961
|
|
|
} |
2962
|
|
|
|
2963
|
|
|
|
2964
|
|
|
// Break the formatting string into an array of chunks |
2965
|
|
|
function chunkFormatString(formatStr) { |
2966
|
|
|
var chunks = []; |
2967
|
|
|
var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination |
2968
|
|
|
var match; |
2969
|
|
|
|
2970
|
|
|
while ((match = chunker.exec(formatStr))) { |
2971
|
|
|
if (match[1]) { // a literal string inside [ ... ] |
2972
|
|
|
chunks.push(match[1]); |
2973
|
|
|
} |
2974
|
|
|
else if (match[2]) { // non-zero formatting inside ( ... ) |
2975
|
|
|
chunks.push({ maybe: chunkFormatString(match[2]) }); |
2976
|
|
|
} |
2977
|
|
|
else if (match[3]) { // a formatting token |
2978
|
|
|
chunks.push({ token: match[3] }); |
2979
|
|
|
} |
2980
|
|
|
else if (match[5]) { // an unenclosed literal string |
2981
|
|
|
chunks.push(match[5]); |
2982
|
|
|
} |
2983
|
|
|
} |
2984
|
|
|
|
2985
|
|
|
return chunks; |
2986
|
|
|
} |
2987
|
|
|
|
2988
|
|
|
;; |
2989
|
|
|
|
2990
|
|
|
/* A rectangular panel that is absolutely positioned over other content |
2991
|
|
|
------------------------------------------------------------------------------------------------------------------------ |
2992
|
|
|
Options: |
2993
|
|
|
- className (string) |
2994
|
|
|
- content (HTML string or jQuery element set) |
2995
|
|
|
- parentEl |
2996
|
|
|
- top |
2997
|
|
|
- left |
2998
|
|
|
- right (the x coord of where the right edge should be. not a "CSS" right) |
2999
|
|
|
- autoHide (boolean) |
3000
|
|
|
- show (callback) |
3001
|
|
|
- hide (callback) |
3002
|
|
|
*/ |
3003
|
|
|
|
3004
|
|
|
function Popover(options) { |
3005
|
|
|
this.options = options || {}; |
3006
|
|
|
} |
3007
|
|
|
|
3008
|
|
|
|
3009
|
|
|
Popover.prototype = { |
3010
|
|
|
|
3011
|
|
|
isHidden: true, |
3012
|
|
|
options: null, |
3013
|
|
|
el: null, // the container element for the popover. generated by this object |
3014
|
|
|
documentMousedownProxy: null, // document mousedown handler bound to `this` |
3015
|
|
|
margin: 10, // the space required between the popover and the edges of the scroll container |
3016
|
|
|
|
3017
|
|
|
|
3018
|
|
|
// Shows the popover on the specified position. Renders it if not already |
3019
|
|
|
show: function() { |
3020
|
|
|
if (this.isHidden) { |
3021
|
|
|
if (!this.el) { |
3022
|
|
|
this.render(); |
3023
|
|
|
} |
3024
|
|
|
this.el.show(); |
3025
|
|
|
this.position(); |
3026
|
|
|
this.isHidden = false; |
3027
|
|
|
this.trigger('show'); |
3028
|
|
|
} |
3029
|
|
|
}, |
3030
|
|
|
|
3031
|
|
|
|
3032
|
|
|
// Hides the popover, through CSS, but does not remove it from the DOM |
3033
|
|
|
hide: function() { |
3034
|
|
|
if (!this.isHidden) { |
3035
|
|
|
this.el.hide(); |
3036
|
|
|
this.isHidden = true; |
3037
|
|
|
this.trigger('hide'); |
3038
|
|
|
} |
3039
|
|
|
}, |
3040
|
|
|
|
3041
|
|
|
|
3042
|
|
|
// Creates `this.el` and renders content inside of it |
3043
|
|
|
render: function() { |
3044
|
|
|
var _this = this; |
3045
|
|
|
var options = this.options; |
3046
|
|
|
|
3047
|
|
|
this.el = $('<div class="fc-popover"/>') |
3048
|
|
|
.addClass(options.className || '') |
3049
|
|
|
.css({ |
3050
|
|
|
// position initially to the top left to avoid creating scrollbars |
3051
|
|
|
top: 0, |
3052
|
|
|
left: 0 |
3053
|
|
|
}) |
3054
|
|
|
.append(options.content) |
3055
|
|
|
.appendTo(options.parentEl); |
3056
|
|
|
|
3057
|
|
|
// when a click happens on anything inside with a 'fc-close' className, hide the popover |
3058
|
|
|
this.el.on('click', '.fc-close', function() { |
3059
|
|
|
_this.hide(); |
3060
|
|
|
}); |
3061
|
|
|
|
3062
|
|
|
if (options.autoHide) { |
3063
|
|
|
$(document).on('mousedown', this.documentMousedownProxy = $.proxy(this, 'documentMousedown')); |
3064
|
|
|
} |
3065
|
|
|
}, |
3066
|
|
|
|
3067
|
|
|
|
3068
|
|
|
// Triggered when the user clicks *anywhere* in the document, for the autoHide feature |
3069
|
|
|
documentMousedown: function(ev) { |
3070
|
|
|
// only hide the popover if the click happened outside the popover |
3071
|
|
|
if (this.el && !$(ev.target).closest(this.el).length) { |
3072
|
|
|
this.hide(); |
3073
|
|
|
} |
3074
|
|
|
}, |
3075
|
|
|
|
3076
|
|
|
|
3077
|
|
|
// Hides and unregisters any handlers |
3078
|
|
|
destroy: function() { |
3079
|
|
|
this.hide(); |
3080
|
|
|
|
3081
|
|
|
if (this.el) { |
3082
|
|
|
this.el.remove(); |
3083
|
|
|
this.el = null; |
3084
|
|
|
} |
3085
|
|
|
|
3086
|
|
|
$(document).off('mousedown', this.documentMousedownProxy); |
3087
|
|
|
}, |
3088
|
|
|
|
3089
|
|
|
|
3090
|
|
|
// Positions the popover optimally, using the top/left/right options |
3091
|
|
|
position: function() { |
3092
|
|
|
var options = this.options; |
3093
|
|
|
var origin = this.el.offsetParent().offset(); |
3094
|
|
|
var width = this.el.outerWidth(); |
3095
|
|
|
var height = this.el.outerHeight(); |
3096
|
|
|
var windowEl = $(window); |
3097
|
|
|
var viewportEl = getScrollParent(this.el); |
3098
|
|
|
var viewportTop; |
3099
|
|
|
var viewportLeft; |
3100
|
|
|
var viewportOffset; |
3101
|
|
|
var top; // the "position" (not "offset") values for the popover |
3102
|
|
|
var left; // |
3103
|
|
|
|
3104
|
|
|
// compute top and left |
3105
|
|
|
top = options.top || 0; |
3106
|
|
|
if (options.left !== undefined) { |
3107
|
|
|
left = options.left; |
3108
|
|
|
} |
3109
|
|
|
else if (options.right !== undefined) { |
3110
|
|
|
left = options.right - width; // derive the left value from the right value |
3111
|
|
|
} |
3112
|
|
|
else { |
3113
|
|
|
left = 0; |
3114
|
|
|
} |
3115
|
|
|
|
3116
|
|
|
if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result |
3117
|
|
|
viewportEl = windowEl; |
3118
|
|
|
viewportTop = 0; // the window is always at the top left |
3119
|
|
|
viewportLeft = 0; // (and .offset() won't work if called here) |
3120
|
|
|
} |
3121
|
|
|
else { |
3122
|
|
|
viewportOffset = viewportEl.offset(); |
3123
|
|
|
viewportTop = viewportOffset.top; |
3124
|
|
|
viewportLeft = viewportOffset.left; |
3125
|
|
|
} |
3126
|
|
|
|
3127
|
|
|
// if the window is scrolled, it causes the visible area to be further down |
3128
|
|
|
viewportTop += windowEl.scrollTop(); |
3129
|
|
|
viewportLeft += windowEl.scrollLeft(); |
3130
|
|
|
|
3131
|
|
|
// constrain to the view port. if constrained by two edges, give precedence to top/left |
3132
|
|
|
if (options.viewportConstrain !== false) { |
3133
|
|
|
top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin); |
3134
|
|
|
top = Math.max(top, viewportTop + this.margin); |
3135
|
|
|
left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin); |
3136
|
|
|
left = Math.max(left, viewportLeft + this.margin); |
3137
|
|
|
} |
3138
|
|
|
|
3139
|
|
|
this.el.css({ |
3140
|
|
|
top: top - origin.top, |
3141
|
|
|
left: left - origin.left |
3142
|
|
|
}); |
3143
|
|
|
}, |
3144
|
|
|
|
3145
|
|
|
|
3146
|
|
|
// Triggers a callback. Calls a function in the option hash of the same name. |
3147
|
|
|
// Arguments beyond the first `name` are forwarded on. |
3148
|
|
|
// TODO: better code reuse for this. Repeat code |
3149
|
|
|
trigger: function(name) { |
3150
|
|
|
if (this.options[name]) { |
3151
|
|
|
this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); |
3152
|
|
|
} |
3153
|
|
|
} |
3154
|
|
|
|
3155
|
|
|
}; |
3156
|
|
|
|
3157
|
|
|
;; |
3158
|
|
|
|
3159
|
|
|
/* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date |
3160
|
|
|
------------------------------------------------------------------------------------------------------------------------ |
3161
|
|
|
Common interface: |
3162
|
|
|
|
3163
|
|
|
CoordMap.prototype = { |
3164
|
|
|
build: function() {}, |
3165
|
|
|
getCell: function(x, y) {} |
3166
|
|
|
}; |
3167
|
|
|
|
3168
|
|
|
*/ |
3169
|
|
|
|
3170
|
|
|
/* Coordinate map for a grid component |
3171
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
3172
|
|
|
|
3173
|
|
|
function GridCoordMap(grid) { |
3174
|
|
|
this.grid = grid; |
3175
|
|
|
} |
3176
|
|
|
|
3177
|
|
|
|
3178
|
|
|
GridCoordMap.prototype = { |
3179
|
|
|
|
3180
|
|
|
grid: null, // reference to the Grid |
3181
|
|
|
rows: null, // the top-to-bottom y coordinates. including the bottom of the last item |
3182
|
|
|
cols: null, // the left-to-right x coordinates. including the right of the last item |
3183
|
|
|
|
3184
|
|
|
containerEl: null, // container element that all coordinates are constrained to. optionally assigned |
3185
|
|
|
minX: null, |
3186
|
|
|
maxX: null, // exclusive |
3187
|
|
|
minY: null, |
3188
|
|
|
maxY: null, // exclusive |
3189
|
|
|
|
3190
|
|
|
|
3191
|
|
|
// Queries the grid for the coordinates of all the cells |
3192
|
|
|
build: function() { |
3193
|
|
|
this.grid.buildCoords( |
3194
|
|
|
this.rows = [], |
3195
|
|
|
this.cols = [] |
3196
|
|
|
); |
3197
|
|
|
this.computeBounds(); |
3198
|
|
|
}, |
3199
|
|
|
|
3200
|
|
|
|
3201
|
|
|
// Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null |
3202
|
|
|
getCell: function(x, y) { |
3203
|
|
|
var cell = null; |
3204
|
|
|
var rows = this.rows; |
3205
|
|
|
var cols = this.cols; |
3206
|
|
|
var r = -1; |
3207
|
|
|
var c = -1; |
3208
|
|
|
var i; |
3209
|
|
|
|
3210
|
|
|
if (this.inBounds(x, y)) { |
3211
|
|
|
|
3212
|
|
|
for (i = 0; i < rows.length; i++) { |
3213
|
|
|
if (y >= rows[i][0] && y < rows[i][1]) { |
3214
|
|
|
r = i; |
3215
|
|
|
break; |
3216
|
|
|
} |
3217
|
|
|
} |
3218
|
|
|
|
3219
|
|
|
for (i = 0; i < cols.length; i++) { |
3220
|
|
|
if (x >= cols[i][0] && x < cols[i][1]) { |
3221
|
|
|
c = i; |
3222
|
|
|
break; |
3223
|
|
|
} |
3224
|
|
|
} |
3225
|
|
|
|
3226
|
|
|
if (r >= 0 && c >= 0) { |
3227
|
|
|
cell = { row: r, col: c }; |
3228
|
|
|
cell.grid = this.grid; |
3229
|
|
|
cell.date = this.grid.getCellDate(cell); |
3230
|
|
|
} |
3231
|
|
|
} |
3232
|
|
|
|
3233
|
|
|
return cell; |
3234
|
|
|
}, |
3235
|
|
|
|
3236
|
|
|
|
3237
|
|
|
// If there is a containerEl, compute the bounds into min/max values |
3238
|
|
|
computeBounds: function() { |
3239
|
|
|
var containerOffset; |
3240
|
|
|
|
3241
|
|
|
if (this.containerEl) { |
3242
|
|
|
containerOffset = this.containerEl.offset(); |
3243
|
|
|
this.minX = containerOffset.left; |
3244
|
|
|
this.maxX = containerOffset.left + this.containerEl.outerWidth(); |
3245
|
|
|
this.minY = containerOffset.top; |
3246
|
|
|
this.maxY = containerOffset.top + this.containerEl.outerHeight(); |
3247
|
|
|
} |
3248
|
|
|
}, |
3249
|
|
|
|
3250
|
|
|
|
3251
|
|
|
// Determines if the given coordinates are in bounds. If no `containerEl`, always true |
3252
|
|
|
inBounds: function(x, y) { |
3253
|
|
|
if (this.containerEl) { |
3254
|
|
|
return x >= this.minX && x < this.maxX && y >= this.minY && y < this.maxY; |
3255
|
|
|
} |
3256
|
|
|
return true; |
3257
|
|
|
} |
3258
|
|
|
|
3259
|
|
|
}; |
3260
|
|
|
|
3261
|
|
|
|
3262
|
|
|
/* Coordinate map that is a combination of multiple other coordinate maps |
3263
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
3264
|
|
|
|
3265
|
|
|
function ComboCoordMap(coordMaps) { |
3266
|
|
|
this.coordMaps = coordMaps; |
3267
|
|
|
} |
3268
|
|
|
|
3269
|
|
|
|
3270
|
|
|
ComboCoordMap.prototype = { |
3271
|
|
|
|
3272
|
|
|
coordMaps: null, // an array of CoordMaps |
3273
|
|
|
|
3274
|
|
|
|
3275
|
|
|
// Builds all coordMaps |
3276
|
|
|
build: function() { |
3277
|
|
|
var coordMaps = this.coordMaps; |
3278
|
|
|
var i; |
3279
|
|
|
|
3280
|
|
|
for (i = 0; i < coordMaps.length; i++) { |
3281
|
|
|
coordMaps[i].build(); |
3282
|
|
|
} |
3283
|
|
|
}, |
3284
|
|
|
|
3285
|
|
|
|
3286
|
|
|
// Queries all coordMaps for the cell underneath the given coordinates, returning the first result |
3287
|
|
|
getCell: function(x, y) { |
3288
|
|
|
var coordMaps = this.coordMaps; |
3289
|
|
|
var cell = null; |
3290
|
|
|
var i; |
3291
|
|
|
|
3292
|
|
|
for (i = 0; i < coordMaps.length && !cell; i++) { |
3293
|
|
|
cell = coordMaps[i].getCell(x, y); |
3294
|
|
|
} |
3295
|
|
|
|
3296
|
|
|
return cell; |
3297
|
|
|
} |
3298
|
|
|
|
3299
|
|
|
}; |
3300
|
|
|
|
3301
|
|
|
;; |
3302
|
|
|
|
3303
|
|
|
/* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over. |
3304
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
3305
|
|
|
// TODO: implement scrolling |
3306
|
|
|
|
3307
|
|
|
function DragListener(coordMap, options) { |
3308
|
|
|
this.coordMap = coordMap; |
3309
|
|
|
this.options = options || {}; |
3310
|
|
|
} |
3311
|
|
|
|
3312
|
|
|
|
3313
|
|
|
DragListener.prototype = { |
3314
|
|
|
|
3315
|
|
|
coordMap: null, |
3316
|
|
|
options: null, |
3317
|
|
|
|
3318
|
|
|
isListening: false, |
3319
|
|
|
isDragging: false, |
3320
|
|
|
|
3321
|
|
|
// the cell/date the mouse was over when listening started |
3322
|
|
|
origCell: null, |
3323
|
|
|
origDate: null, |
3324
|
|
|
|
3325
|
|
|
// the cell/date the mouse is over |
3326
|
|
|
cell: null, |
3327
|
|
|
date: null, |
3328
|
|
|
|
3329
|
|
|
// coordinates of the initial mousedown |
3330
|
|
|
mouseX0: null, |
3331
|
|
|
mouseY0: null, |
3332
|
|
|
|
3333
|
|
|
// handler attached to the document, bound to the DragListener's `this` |
3334
|
|
|
mousemoveProxy: null, |
3335
|
|
|
mouseupProxy: null, |
3336
|
|
|
|
3337
|
|
|
scrollEl: null, |
3338
|
|
|
scrollBounds: null, // { top, bottom, left, right } |
3339
|
|
|
scrollTopVel: null, // pixels per second |
3340
|
|
|
scrollLeftVel: null, // pixels per second |
3341
|
|
|
scrollIntervalId: null, // ID of setTimeout for scrolling animation loop |
3342
|
|
|
scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled |
3343
|
|
|
|
3344
|
|
|
scrollSensitivity: 30, // pixels from edge for scrolling to start |
3345
|
|
|
scrollSpeed: 200, // pixels per second, at maximum speed |
3346
|
|
|
scrollIntervalMs: 50, // millisecond wait between scroll increment |
3347
|
|
|
|
3348
|
|
|
|
3349
|
|
|
// Call this when the user does a mousedown. Will probably lead to startListening |
3350
|
|
|
mousedown: function(ev) { |
3351
|
|
|
if (isPrimaryMouseButton(ev)) { |
3352
|
|
|
|
3353
|
|
|
ev.preventDefault(); // prevents native selection in most browsers |
3354
|
|
|
|
3355
|
|
|
this.startListening(ev); |
3356
|
|
|
|
3357
|
|
|
// start the drag immediately if there is no minimum distance for a drag start |
3358
|
|
|
if (!this.options.distance) { |
3359
|
|
|
this.startDrag(ev); |
3360
|
|
|
} |
3361
|
|
|
} |
3362
|
|
|
}, |
3363
|
|
|
|
3364
|
|
|
|
3365
|
|
|
// Call this to start tracking mouse movements |
3366
|
|
|
startListening: function(ev) { |
3367
|
|
|
var scrollParent; |
3368
|
|
|
var cell; |
3369
|
|
|
|
3370
|
|
|
if (!this.isListening) { |
3371
|
|
|
|
3372
|
|
|
// grab scroll container and attach handler |
3373
|
|
|
if (ev && this.options.scroll) { |
3374
|
|
|
scrollParent = getScrollParent($(ev.target)); |
3375
|
|
|
if (!scrollParent.is(window) && !scrollParent.is(document)) { |
3376
|
|
|
this.scrollEl = scrollParent; |
3377
|
|
|
|
3378
|
|
|
// scope to `this`, and use `debounce` to make sure rapid calls don't happen |
3379
|
|
|
this.scrollHandlerProxy = debounce($.proxy(this, 'scrollHandler'), 100); |
3380
|
|
|
this.scrollEl.on('scroll', this.scrollHandlerProxy); |
3381
|
|
|
} |
3382
|
|
|
} |
3383
|
|
|
|
3384
|
|
|
this.computeCoords(); // relies on `scrollEl` |
3385
|
|
|
|
3386
|
|
|
// get info on the initial cell, date, and coordinates |
3387
|
|
|
if (ev) { |
3388
|
|
|
cell = this.getCell(ev); |
3389
|
|
|
this.origCell = cell; |
3390
|
|
|
this.origDate = cell ? cell.date : null; |
3391
|
|
|
|
3392
|
|
|
this.mouseX0 = ev.pageX; |
3393
|
|
|
this.mouseY0 = ev.pageY; |
3394
|
|
|
} |
3395
|
|
|
|
3396
|
|
|
$(document) |
3397
|
|
|
.on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')) |
3398
|
|
|
.on('mouseup', this.mouseupProxy = $.proxy(this, 'mouseup')) |
3399
|
|
|
.on('selectstart', this.preventDefault); // prevents native selection in IE<=8 |
3400
|
|
|
|
3401
|
|
|
this.isListening = true; |
3402
|
|
|
this.trigger('listenStart', ev); |
3403
|
|
|
} |
3404
|
|
|
}, |
3405
|
|
|
|
3406
|
|
|
|
3407
|
|
|
// Recomputes the drag-critical positions of elements |
3408
|
|
|
computeCoords: function() { |
3409
|
|
|
this.coordMap.build(); |
3410
|
|
|
this.computeScrollBounds(); |
3411
|
|
|
}, |
3412
|
|
|
|
3413
|
|
|
|
3414
|
|
|
// Called when the user moves the mouse |
3415
|
|
|
mousemove: function(ev) { |
3416
|
|
|
var minDistance; |
3417
|
|
|
var distanceSq; // current distance from mouseX0/mouseY0, squared |
3418
|
|
|
|
3419
|
|
|
if (!this.isDragging) { // if not already dragging... |
3420
|
|
|
// then start the drag if the minimum distance criteria is met |
3421
|
|
|
minDistance = this.options.distance || 1; |
3422
|
|
|
distanceSq = Math.pow(ev.pageX - this.mouseX0, 2) + Math.pow(ev.pageY - this.mouseY0, 2); |
3423
|
|
|
if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem |
3424
|
|
|
this.startDrag(ev); |
3425
|
|
|
} |
3426
|
|
|
} |
3427
|
|
|
|
3428
|
|
|
if (this.isDragging) { |
3429
|
|
|
this.drag(ev); // report a drag, even if this mousemove initiated the drag |
3430
|
|
|
} |
3431
|
|
|
}, |
3432
|
|
|
|
3433
|
|
|
|
3434
|
|
|
// Call this to initiate a legitimate drag. |
3435
|
|
|
// This function is called internally from this class, but can also be called explicitly from outside |
3436
|
|
|
startDrag: function(ev) { |
3437
|
|
|
var cell; |
3438
|
|
|
|
3439
|
|
|
if (!this.isListening) { // startDrag must have manually initiated |
3440
|
|
|
this.startListening(); |
3441
|
|
|
} |
3442
|
|
|
|
3443
|
|
|
if (!this.isDragging) { |
3444
|
|
|
this.isDragging = true; |
3445
|
|
|
this.trigger('dragStart', ev); |
3446
|
|
|
|
3447
|
|
|
// report the initial cell the mouse is over |
3448
|
|
|
cell = this.getCell(ev); |
3449
|
|
|
if (cell) { |
3450
|
|
|
this.cellOver(cell, true); |
3451
|
|
|
} |
3452
|
|
|
} |
3453
|
|
|
}, |
3454
|
|
|
|
3455
|
|
|
|
3456
|
|
|
// Called while the mouse is being moved and when we know a legitimate drag is taking place |
3457
|
|
|
drag: function(ev) { |
3458
|
|
|
var cell; |
3459
|
|
|
|
3460
|
|
|
if (this.isDragging) { |
3461
|
|
|
cell = this.getCell(ev); |
3462
|
|
|
|
3463
|
|
|
if (!isCellsEqual(cell, this.cell)) { // a different cell than before? |
3464
|
|
|
if (this.cell) { |
3465
|
|
|
this.cellOut(); |
3466
|
|
|
} |
3467
|
|
|
if (cell) { |
3468
|
|
|
this.cellOver(cell); |
3469
|
|
|
} |
3470
|
|
|
} |
3471
|
|
|
|
3472
|
|
|
this.dragScroll(ev); // will possibly cause scrolling |
3473
|
|
|
} |
3474
|
|
|
}, |
3475
|
|
|
|
3476
|
|
|
|
3477
|
|
|
// Called when a the mouse has just moved over a new cell |
3478
|
|
|
cellOver: function(cell) { |
3479
|
|
|
this.cell = cell; |
3480
|
|
|
this.date = cell.date; |
3481
|
|
|
this.trigger('cellOver', cell, cell.date); |
3482
|
|
|
}, |
3483
|
|
|
|
3484
|
|
|
|
3485
|
|
|
// Called when the mouse has just moved out of a cell |
3486
|
|
|
cellOut: function() { |
3487
|
|
|
if (this.cell) { |
3488
|
|
|
this.trigger('cellOut', this.cell); |
3489
|
|
|
this.cell = null; |
3490
|
|
|
this.date = null; |
3491
|
|
|
} |
3492
|
|
|
}, |
3493
|
|
|
|
3494
|
|
|
|
3495
|
|
|
// Called when the user does a mouseup |
3496
|
|
|
mouseup: function(ev) { |
3497
|
|
|
this.stopDrag(ev); |
3498
|
|
|
this.stopListening(ev); |
3499
|
|
|
}, |
3500
|
|
|
|
3501
|
|
|
|
3502
|
|
|
// Called when the drag is over. Will not cause listening to stop however. |
3503
|
|
|
// A concluding 'cellOut' event will NOT be triggered. |
3504
|
|
|
stopDrag: function(ev) { |
3505
|
|
|
if (this.isDragging) { |
3506
|
|
|
this.stopScrolling(); |
3507
|
|
|
this.trigger('dragStop', ev); |
3508
|
|
|
this.isDragging = false; |
3509
|
|
|
} |
3510
|
|
|
}, |
3511
|
|
|
|
3512
|
|
|
|
3513
|
|
|
// Call this to stop listening to the user's mouse events |
3514
|
|
|
stopListening: function(ev) { |
3515
|
|
|
if (this.isListening) { |
3516
|
|
|
|
3517
|
|
|
// remove the scroll handler if there is a scrollEl |
3518
|
|
|
if (this.scrollEl) { |
3519
|
|
|
this.scrollEl.off('scroll', this.scrollHandlerProxy); |
3520
|
|
|
this.scrollHandlerProxy = null; |
3521
|
|
|
} |
3522
|
|
|
|
3523
|
|
|
$(document) |
3524
|
|
|
.off('mousemove', this.mousemoveProxy) |
3525
|
|
|
.off('mouseup', this.mouseupProxy) |
3526
|
|
|
.off('selectstart', this.preventDefault); |
3527
|
|
|
|
3528
|
|
|
this.mousemoveProxy = null; |
3529
|
|
|
this.mouseupProxy = null; |
3530
|
|
|
|
3531
|
|
|
this.isListening = false; |
3532
|
|
|
this.trigger('listenStop', ev); |
3533
|
|
|
|
3534
|
|
|
this.origCell = this.cell = null; |
3535
|
|
|
this.origDate = this.date = null; |
3536
|
|
|
} |
3537
|
|
|
}, |
3538
|
|
|
|
3539
|
|
|
|
3540
|
|
|
// Gets the cell underneath the coordinates for the given mouse event |
3541
|
|
|
getCell: function(ev) { |
3542
|
|
|
return this.coordMap.getCell(ev.pageX, ev.pageY); |
3543
|
|
|
}, |
3544
|
|
|
|
3545
|
|
|
|
3546
|
|
|
// Triggers a callback. Calls a function in the option hash of the same name. |
3547
|
|
|
// Arguments beyond the first `name` are forwarded on. |
3548
|
|
|
trigger: function(name) { |
3549
|
|
|
if (this.options[name]) { |
3550
|
|
|
this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); |
3551
|
|
|
} |
3552
|
|
|
}, |
3553
|
|
|
|
3554
|
|
|
|
3555
|
|
|
// Stops a given mouse event from doing it's native browser action. In our case, text selection. |
3556
|
|
|
preventDefault: function(ev) { |
3557
|
|
|
ev.preventDefault(); |
3558
|
|
|
}, |
3559
|
|
|
|
3560
|
|
|
|
3561
|
|
|
/* Scrolling |
3562
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
3563
|
|
|
|
3564
|
|
|
|
3565
|
|
|
// Computes and stores the bounding rectangle of scrollEl |
3566
|
|
|
computeScrollBounds: function() { |
3567
|
|
|
var el = this.scrollEl; |
3568
|
|
|
var offset; |
3569
|
|
|
|
3570
|
|
|
if (el) { |
3571
|
|
|
offset = el.offset(); |
3572
|
|
|
this.scrollBounds = { |
3573
|
|
|
top: offset.top, |
3574
|
|
|
left: offset.left, |
3575
|
|
|
bottom: offset.top + el.outerHeight(), |
3576
|
|
|
right: offset.left + el.outerWidth() |
3577
|
|
|
}; |
3578
|
|
|
} |
3579
|
|
|
}, |
3580
|
|
|
|
3581
|
|
|
|
3582
|
|
|
// Called when the dragging is in progress and scrolling should be updated |
3583
|
|
|
dragScroll: function(ev) { |
3584
|
|
|
var sensitivity = this.scrollSensitivity; |
3585
|
|
|
var bounds = this.scrollBounds; |
3586
|
|
|
var topCloseness, bottomCloseness; |
3587
|
|
|
var leftCloseness, rightCloseness; |
3588
|
|
|
var topVel = 0; |
3589
|
|
|
var leftVel = 0; |
3590
|
|
|
|
3591
|
|
|
if (bounds) { // only scroll if scrollEl exists |
3592
|
|
|
|
3593
|
|
|
// compute closeness to edges. valid range is from 0.0 - 1.0 |
3594
|
|
|
topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity; |
3595
|
|
|
bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity; |
3596
|
|
|
leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity; |
3597
|
|
|
rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity; |
3598
|
|
|
|
3599
|
|
|
// translate vertical closeness into velocity. |
3600
|
|
|
// mouse must be completely in bounds for velocity to happen. |
3601
|
|
|
if (topCloseness >= 0 && topCloseness <= 1) { |
3602
|
|
|
topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up |
3603
|
|
|
} |
3604
|
|
|
else if (bottomCloseness >= 0 && bottomCloseness <= 1) { |
3605
|
|
|
topVel = bottomCloseness * this.scrollSpeed; |
3606
|
|
|
} |
3607
|
|
|
|
3608
|
|
|
// translate horizontal closeness into velocity |
3609
|
|
|
if (leftCloseness >= 0 && leftCloseness <= 1) { |
3610
|
|
|
leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left |
3611
|
|
|
} |
3612
|
|
|
else if (rightCloseness >= 0 && rightCloseness <= 1) { |
3613
|
|
|
leftVel = rightCloseness * this.scrollSpeed; |
3614
|
|
|
} |
3615
|
|
|
} |
3616
|
|
|
|
3617
|
|
|
this.setScrollVel(topVel, leftVel); |
3618
|
|
|
}, |
3619
|
|
|
|
3620
|
|
|
|
3621
|
|
|
// Sets the speed-of-scrolling for the scrollEl |
3622
|
|
|
setScrollVel: function(topVel, leftVel) { |
3623
|
|
|
|
3624
|
|
|
this.scrollTopVel = topVel; |
3625
|
|
|
this.scrollLeftVel = leftVel; |
3626
|
|
|
|
3627
|
|
|
this.constrainScrollVel(); // massages into realistic values |
3628
|
|
|
|
3629
|
|
|
// if there is non-zero velocity, and an animation loop hasn't already started, then START |
3630
|
|
|
if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) { |
3631
|
|
|
this.scrollIntervalId = setInterval( |
3632
|
|
|
$.proxy(this, 'scrollIntervalFunc'), // scope to `this` |
3633
|
|
|
this.scrollIntervalMs |
3634
|
|
|
); |
3635
|
|
|
} |
3636
|
|
|
}, |
3637
|
|
|
|
3638
|
|
|
|
3639
|
|
|
// Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way |
3640
|
|
|
constrainScrollVel: function() { |
3641
|
|
|
var el = this.scrollEl; |
3642
|
|
|
|
3643
|
|
|
if (this.scrollTopVel < 0) { // scrolling up? |
3644
|
|
|
if (el.scrollTop() <= 0) { // already scrolled all the way up? |
3645
|
|
|
this.scrollTopVel = 0; |
3646
|
|
|
} |
3647
|
|
|
} |
3648
|
|
|
else if (this.scrollTopVel > 0) { // scrolling down? |
3649
|
|
|
if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down? |
3650
|
|
|
this.scrollTopVel = 0; |
3651
|
|
|
} |
3652
|
|
|
} |
3653
|
|
|
|
3654
|
|
|
if (this.scrollLeftVel < 0) { // scrolling left? |
3655
|
|
|
if (el.scrollLeft() <= 0) { // already scrolled all the left? |
3656
|
|
|
this.scrollLeftVel = 0; |
3657
|
|
|
} |
3658
|
|
|
} |
3659
|
|
|
else if (this.scrollLeftVel > 0) { // scrolling right? |
3660
|
|
|
if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right? |
3661
|
|
|
this.scrollLeftVel = 0; |
3662
|
|
|
} |
3663
|
|
|
} |
3664
|
|
|
}, |
3665
|
|
|
|
3666
|
|
|
|
3667
|
|
|
// This function gets called during every iteration of the scrolling animation loop |
3668
|
|
|
scrollIntervalFunc: function() { |
3669
|
|
|
var el = this.scrollEl; |
3670
|
|
|
var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by |
3671
|
|
|
|
3672
|
|
|
// change the value of scrollEl's scroll |
3673
|
|
|
if (this.scrollTopVel) { |
3674
|
|
|
el.scrollTop(el.scrollTop() + this.scrollTopVel * frac); |
3675
|
|
|
} |
3676
|
|
|
if (this.scrollLeftVel) { |
3677
|
|
|
el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac); |
3678
|
|
|
} |
3679
|
|
|
|
3680
|
|
|
this.constrainScrollVel(); // since the scroll values changed, recompute the velocities |
3681
|
|
|
|
3682
|
|
|
// if scrolled all the way, which causes the vels to be zero, stop the animation loop |
3683
|
|
|
if (!this.scrollTopVel && !this.scrollLeftVel) { |
3684
|
|
|
this.stopScrolling(); |
3685
|
|
|
} |
3686
|
|
|
}, |
3687
|
|
|
|
3688
|
|
|
|
3689
|
|
|
// Kills any existing scrolling animation loop |
3690
|
|
|
stopScrolling: function() { |
3691
|
|
|
if (this.scrollIntervalId) { |
3692
|
|
|
clearInterval(this.scrollIntervalId); |
3693
|
|
|
this.scrollIntervalId = null; |
3694
|
|
|
|
3695
|
|
|
// when all done with scrolling, recompute positions since they probably changed |
3696
|
|
|
this.computeCoords(); |
3697
|
|
|
} |
3698
|
|
|
}, |
3699
|
|
|
|
3700
|
|
|
|
3701
|
|
|
// Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce) |
3702
|
|
|
scrollHandler: function() { |
3703
|
|
|
// recompute all coordinates, but *only* if this is *not* part of our scrolling animation |
3704
|
|
|
if (!this.scrollIntervalId) { |
3705
|
|
|
this.computeCoords(); |
3706
|
|
|
} |
3707
|
|
|
} |
3708
|
|
|
|
3709
|
|
|
}; |
3710
|
|
|
|
3711
|
|
|
|
3712
|
|
|
// Returns `true` if the cells are identically equal. `false` otherwise. |
3713
|
|
|
// They must have the same row, col, and be from the same grid. |
3714
|
|
|
// Two null values will be considered equal, as two "out of the grid" states are the same. |
3715
|
|
|
function isCellsEqual(cell1, cell2) { |
3716
|
|
|
|
3717
|
|
|
if (!cell1 && !cell2) { |
3718
|
|
|
return true; |
3719
|
|
|
} |
3720
|
|
|
|
3721
|
|
|
if (cell1 && cell2) { |
3722
|
|
|
return cell1.grid === cell2.grid && |
3723
|
|
|
cell1.row === cell2.row && |
3724
|
|
|
cell1.col === cell2.col; |
3725
|
|
|
} |
3726
|
|
|
|
3727
|
|
|
return false; |
3728
|
|
|
} |
3729
|
|
|
|
3730
|
|
|
;; |
3731
|
|
|
|
3732
|
|
|
/* Creates a clone of an element and lets it track the mouse as it moves |
3733
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
3734
|
|
|
|
3735
|
|
|
function MouseFollower(sourceEl, options) { |
3736
|
|
|
this.options = options = options || {}; |
3737
|
|
|
this.sourceEl = sourceEl; |
3738
|
|
|
this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent |
3739
|
|
|
} |
3740
|
|
|
|
3741
|
|
|
|
3742
|
|
|
MouseFollower.prototype = { |
3743
|
|
|
|
3744
|
|
|
options: null, |
3745
|
|
|
|
3746
|
|
|
sourceEl: null, // the element that will be cloned and made to look like it is dragging |
3747
|
|
|
el: null, // the clone of `sourceEl` that will track the mouse |
3748
|
|
|
parentEl: null, // the element that `el` (the clone) will be attached to |
3749
|
|
|
|
3750
|
|
|
// the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl |
3751
|
|
|
top0: null, |
3752
|
|
|
left0: null, |
3753
|
|
|
|
3754
|
|
|
// the initial position of the mouse |
3755
|
|
|
mouseY0: null, |
3756
|
|
|
mouseX0: null, |
3757
|
|
|
|
3758
|
|
|
// the number of pixels the mouse has moved from its initial position |
3759
|
|
|
topDelta: null, |
3760
|
|
|
leftDelta: null, |
3761
|
|
|
|
3762
|
|
|
mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this` |
3763
|
|
|
|
3764
|
|
|
isFollowing: false, |
3765
|
|
|
isHidden: false, |
3766
|
|
|
isAnimating: false, // doing the revert animation? |
3767
|
|
|
|
3768
|
|
|
|
3769
|
|
|
// Causes the element to start following the mouse |
3770
|
|
|
start: function(ev) { |
3771
|
|
|
if (!this.isFollowing) { |
3772
|
|
|
this.isFollowing = true; |
3773
|
|
|
|
3774
|
|
|
this.mouseY0 = ev.pageY; |
3775
|
|
|
this.mouseX0 = ev.pageX; |
3776
|
|
|
this.topDelta = 0; |
3777
|
|
|
this.leftDelta = 0; |
3778
|
|
|
|
3779
|
|
|
if (!this.isHidden) { |
3780
|
|
|
this.updatePosition(); |
3781
|
|
|
} |
3782
|
|
|
|
3783
|
|
|
$(document).on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove')); |
3784
|
|
|
} |
3785
|
|
|
}, |
3786
|
|
|
|
3787
|
|
|
|
3788
|
|
|
// Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position. |
3789
|
|
|
// `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately. |
3790
|
|
|
stop: function(shouldRevert, callback) { |
3791
|
|
|
var _this = this; |
3792
|
|
|
var revertDuration = this.options.revertDuration; |
3793
|
|
|
|
3794
|
|
|
function complete() { |
3795
|
|
|
this.isAnimating = false; |
3796
|
|
|
_this.destroyEl(); |
3797
|
|
|
|
3798
|
|
|
this.top0 = this.left0 = null; // reset state for future updatePosition calls |
3799
|
|
|
|
3800
|
|
|
if (callback) { |
3801
|
|
|
callback(); |
3802
|
|
|
} |
3803
|
|
|
} |
3804
|
|
|
|
3805
|
|
|
if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time |
3806
|
|
|
this.isFollowing = false; |
3807
|
|
|
|
3808
|
|
|
$(document).off('mousemove', this.mousemoveProxy); |
3809
|
|
|
|
3810
|
|
|
if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation? |
3811
|
|
|
this.isAnimating = true; |
3812
|
|
|
this.el.animate({ |
3813
|
|
|
top: this.top0, |
3814
|
|
|
left: this.left0 |
3815
|
|
|
}, { |
3816
|
|
|
duration: revertDuration, |
3817
|
|
|
complete: complete |
3818
|
|
|
}); |
3819
|
|
|
} |
3820
|
|
|
else { |
3821
|
|
|
complete(); |
3822
|
|
|
} |
3823
|
|
|
} |
3824
|
|
|
}, |
3825
|
|
|
|
3826
|
|
|
|
3827
|
|
|
// Gets the tracking element. Create it if necessary |
3828
|
|
|
getEl: function() { |
3829
|
|
|
var el = this.el; |
3830
|
|
|
|
3831
|
|
|
if (!el) { |
3832
|
|
|
this.sourceEl.width(); // hack to force IE8 to compute correct bounding box |
3833
|
|
|
el = this.el = this.sourceEl.clone() |
3834
|
|
|
.css({ |
3835
|
|
|
position: 'absolute', |
3836
|
|
|
visibility: '', // in case original element was hidden (commonly through hideEvents()) |
3837
|
|
|
display: this.isHidden ? 'none' : '', // for when initially hidden |
3838
|
|
|
margin: 0, |
3839
|
|
|
right: 'auto', // erase and set width instead |
3840
|
|
|
bottom: 'auto', // erase and set height instead |
3841
|
|
|
width: this.sourceEl.width(), // explicit height in case there was a 'right' value |
3842
|
|
|
height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value |
3843
|
|
|
opacity: this.options.opacity || '', |
3844
|
|
|
zIndex: this.options.zIndex |
3845
|
|
|
}) |
3846
|
|
|
.appendTo(this.parentEl); |
3847
|
|
|
} |
3848
|
|
|
|
3849
|
|
|
return el; |
3850
|
|
|
}, |
3851
|
|
|
|
3852
|
|
|
|
3853
|
|
|
// Removes the tracking element if it has already been created |
3854
|
|
|
destroyEl: function() { |
3855
|
|
|
if (this.el) { |
3856
|
|
|
this.el.remove(); |
3857
|
|
|
this.el = null; |
3858
|
|
|
} |
3859
|
|
|
}, |
3860
|
|
|
|
3861
|
|
|
|
3862
|
|
|
// Update the CSS position of the tracking element |
3863
|
|
|
updatePosition: function() { |
3864
|
|
|
var sourceOffset; |
3865
|
|
|
var origin; |
3866
|
|
|
|
3867
|
|
|
this.getEl(); // ensure this.el |
3868
|
|
|
|
3869
|
|
|
// make sure origin info was computed |
3870
|
|
|
if (this.top0 === null) { |
3871
|
|
|
this.sourceEl.width(); // hack to force IE8 to compute correct bounding box |
3872
|
|
|
sourceOffset = this.sourceEl.offset(); |
3873
|
|
|
origin = this.el.offsetParent().offset(); |
3874
|
|
|
this.top0 = sourceOffset.top - origin.top; |
3875
|
|
|
this.left0 = sourceOffset.left - origin.left; |
3876
|
|
|
} |
3877
|
|
|
|
3878
|
|
|
this.el.css({ |
3879
|
|
|
top: this.top0 + this.topDelta, |
3880
|
|
|
left: this.left0 + this.leftDelta |
3881
|
|
|
}); |
3882
|
|
|
}, |
3883
|
|
|
|
3884
|
|
|
|
3885
|
|
|
// Gets called when the user moves the mouse |
3886
|
|
|
mousemove: function(ev) { |
3887
|
|
|
this.topDelta = ev.pageY - this.mouseY0; |
3888
|
|
|
this.leftDelta = ev.pageX - this.mouseX0; |
3889
|
|
|
|
3890
|
|
|
if (!this.isHidden) { |
3891
|
|
|
this.updatePosition(); |
3892
|
|
|
} |
3893
|
|
|
}, |
3894
|
|
|
|
3895
|
|
|
|
3896
|
|
|
// Temporarily makes the tracking element invisible. Can be called before following starts |
3897
|
|
|
hide: function() { |
3898
|
|
|
if (!this.isHidden) { |
3899
|
|
|
this.isHidden = true; |
3900
|
|
|
if (this.el) { |
3901
|
|
|
this.el.hide(); |
3902
|
|
|
} |
3903
|
|
|
} |
3904
|
|
|
}, |
3905
|
|
|
|
3906
|
|
|
|
3907
|
|
|
// Show the tracking element after it has been temporarily hidden |
3908
|
|
|
show: function() { |
3909
|
|
|
if (this.isHidden) { |
3910
|
|
|
this.isHidden = false; |
3911
|
|
|
this.updatePosition(); |
3912
|
|
|
this.getEl().show(); |
3913
|
|
|
} |
3914
|
|
|
} |
3915
|
|
|
|
3916
|
|
|
}; |
3917
|
|
|
|
3918
|
|
|
;; |
3919
|
|
|
|
3920
|
|
|
/* A utility class for rendering <tr> rows. |
3921
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
3922
|
|
|
// It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type" |
3923
|
|
|
// (such as highlight rows, day rows, helper rows, etc). |
3924
|
|
|
|
3925
|
|
|
function RowRenderer(view) { |
3926
|
|
|
this.view = view; |
3927
|
|
|
} |
3928
|
|
|
|
3929
|
|
|
|
3930
|
|
|
RowRenderer.prototype = { |
3931
|
|
|
|
3932
|
|
|
view: null, // a View object |
3933
|
|
|
cellHtml: '<td/>', // plain default HTML used for a cell when no other is available |
3934
|
|
|
|
3935
|
|
|
|
3936
|
|
|
// Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`. |
3937
|
|
|
// Also applies the "intro" and "outro" cells, which are specified by the subclass and views. |
3938
|
|
|
// `row` is an optional row number. |
3939
|
|
|
rowHtml: function(rowType, row) { |
3940
|
|
|
var view = this.view; |
3941
|
|
|
var renderCell = this.getHtmlRenderer('cell', rowType); |
3942
|
|
|
var cellHtml = ''; |
3943
|
|
|
var col; |
3944
|
|
|
var date; |
3945
|
|
|
|
3946
|
|
|
row = row || 0; |
3947
|
|
|
|
3948
|
|
|
for (col = 0; col < view.colCnt; col++) { |
3949
|
|
|
date = view.cellToDate(row, col); |
3950
|
|
|
cellHtml += renderCell(row, col, date); |
3951
|
|
|
} |
3952
|
|
|
|
3953
|
|
|
cellHtml = this.bookendCells(cellHtml, rowType, row); // apply intro and outro |
3954
|
|
|
|
3955
|
|
|
return '<tr>' + cellHtml + '</tr>'; |
3956
|
|
|
}, |
3957
|
|
|
|
3958
|
|
|
|
3959
|
|
|
// Applies the "intro" and "outro" HTML to the given cells. |
3960
|
|
|
// Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro. |
3961
|
|
|
// `cells` can be an HTML string of <td>'s or a jQuery <tr> element |
3962
|
|
|
// `row` is an optional row number. |
3963
|
|
|
bookendCells: function(cells, rowType, row) { |
3964
|
|
|
var view = this.view; |
3965
|
|
|
var intro = this.getHtmlRenderer('intro', rowType)(row || 0); |
3966
|
|
|
var outro = this.getHtmlRenderer('outro', rowType)(row || 0); |
3967
|
|
|
var isRTL = view.opt('isRTL'); |
3968
|
|
|
var prependHtml = isRTL ? outro : intro; |
3969
|
|
|
var appendHtml = isRTL ? intro : outro; |
3970
|
|
|
|
3971
|
|
|
if (typeof cells === 'string') { |
3972
|
|
|
return prependHtml + cells + appendHtml; |
3973
|
|
|
} |
3974
|
|
|
else { // a jQuery <tr> element |
3975
|
|
|
return cells.prepend(prependHtml).append(appendHtml); |
3976
|
|
|
} |
3977
|
|
|
}, |
3978
|
|
|
|
3979
|
|
|
|
3980
|
|
|
// Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific |
3981
|
|
|
// `rowType` (like day, eventSkeleton, helperSkeleton), which is optional. |
3982
|
|
|
// If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer. |
3983
|
|
|
// We will query the View object first for any custom rendering functions, then the methods of the subclass. |
3984
|
|
|
getHtmlRenderer: function(rendererName, rowType) { |
3985
|
|
|
var view = this.view; |
3986
|
|
|
var generalName; // like "cellHtml" |
3987
|
|
|
var specificName; // like "dayCellHtml". based on rowType |
3988
|
|
|
var provider; // either the View or the RowRenderer subclass, whichever provided the method |
3989
|
|
|
var renderer; |
3990
|
|
|
|
3991
|
|
|
generalName = rendererName + 'Html'; |
3992
|
|
|
if (rowType) { |
3993
|
|
|
specificName = rowType + capitaliseFirstLetter(rendererName) + 'Html'; |
3994
|
|
|
} |
3995
|
|
|
|
3996
|
|
|
if (specificName && (renderer = view[specificName])) { |
3997
|
|
|
provider = view; |
3998
|
|
|
} |
3999
|
|
|
else if (specificName && (renderer = this[specificName])) { |
4000
|
|
|
provider = this; |
4001
|
|
|
} |
4002
|
|
|
else if ((renderer = view[generalName])) { |
4003
|
|
|
provider = view; |
4004
|
|
|
} |
4005
|
|
|
else if ((renderer = this[generalName])) { |
4006
|
|
|
provider = this; |
4007
|
|
|
} |
4008
|
|
|
|
4009
|
|
|
if (typeof renderer === 'function') { |
4010
|
|
|
return function(row) { |
4011
|
|
|
return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string |
|
|
|
|
4012
|
|
|
}; |
4013
|
|
|
} |
4014
|
|
|
|
4015
|
|
|
// the rendered can be a plain string as well. if not specified, always an empty string. |
4016
|
|
|
return function() { |
4017
|
|
|
return renderer || ''; |
4018
|
|
|
}; |
4019
|
|
|
} |
4020
|
|
|
|
4021
|
|
|
}; |
4022
|
|
|
|
4023
|
|
|
;; |
4024
|
|
|
|
4025
|
|
|
/* An abstract class comprised of a "grid" of cells that each represent a specific datetime |
4026
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
4027
|
|
|
|
4028
|
|
|
function Grid(view) { |
4029
|
|
|
RowRenderer.call(this, view); // call the super-constructor |
4030
|
|
|
this.coordMap = new GridCoordMap(this); |
4031
|
|
|
} |
4032
|
|
|
|
4033
|
|
|
|
4034
|
|
|
Grid.prototype = createObject(RowRenderer.prototype); // declare the super-class |
4035
|
|
|
$.extend(Grid.prototype, { |
4036
|
|
|
|
4037
|
|
|
el: null, // the containing element |
4038
|
|
|
coordMap: null, // a GridCoordMap that converts pixel values to datetimes |
4039
|
|
|
cellDuration: null, // a cell's duration. subclasses must assign this ASAP |
4040
|
|
|
|
4041
|
|
|
|
4042
|
|
|
// Renders the grid into the `el` element. |
4043
|
|
|
// Subclasses should override and call this super-method when done. |
4044
|
|
|
render: function() { |
4045
|
|
|
this.bindHandlers(); |
4046
|
|
|
}, |
4047
|
|
|
|
4048
|
|
|
|
4049
|
|
|
// Called when the grid's resources need to be cleaned up |
4050
|
|
|
destroy: function() { |
4051
|
|
|
// subclasses can implement |
4052
|
|
|
}, |
4053
|
|
|
|
4054
|
|
|
|
4055
|
|
|
/* Coordinates & Cells |
4056
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4057
|
|
|
|
4058
|
|
|
|
4059
|
|
|
// Populates the given empty arrays with the y and x coordinates of the cells |
4060
|
|
|
buildCoords: function(rows, cols) { |
4061
|
|
|
// subclasses must implement |
4062
|
|
|
}, |
4063
|
|
|
|
4064
|
|
|
|
4065
|
|
|
// Given a cell object, returns the date for that cell |
4066
|
|
|
getCellDate: function(cell) { |
4067
|
|
|
// subclasses must implement |
4068
|
|
|
}, |
4069
|
|
|
|
4070
|
|
|
|
4071
|
|
|
// Given a cell object, returns the element that represents the cell's whole-day |
4072
|
|
|
getCellDayEl: function(cell) { |
4073
|
|
|
// subclasses must implement |
4074
|
|
|
}, |
4075
|
|
|
|
4076
|
|
|
|
4077
|
|
|
// Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects |
4078
|
|
|
rangeToSegs: function(start, end) { |
4079
|
|
|
// subclasses must implement |
4080
|
|
|
}, |
4081
|
|
|
|
4082
|
|
|
|
4083
|
|
|
/* Handlers |
4084
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4085
|
|
|
|
4086
|
|
|
|
4087
|
|
|
// Attach handlers to `this.el`, using bubbling to listen to all ancestors. |
4088
|
|
|
// We don't need to undo any of this in a "destroy" method, because the view will simply remove `this.el` from the |
4089
|
|
|
// DOM and jQuery will be smart enough to garbage collect the handlers. |
4090
|
|
|
bindHandlers: function() { |
4091
|
|
|
var _this = this; |
4092
|
|
|
|
4093
|
|
|
this.el.on('mousedown', function(ev) { |
4094
|
|
|
if ( |
4095
|
|
|
!$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link |
4096
|
|
|
!$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one) |
4097
|
|
|
) { |
4098
|
|
|
_this.dayMousedown(ev); |
4099
|
|
|
} |
4100
|
|
|
}); |
4101
|
|
|
|
4102
|
|
|
this.bindSegHandlers(); // attach event-element-related handlers. in Grid.events.js |
4103
|
|
|
}, |
4104
|
|
|
|
4105
|
|
|
|
4106
|
|
|
// Process a mousedown on an element that represents a day. For day clicking and selecting. |
4107
|
|
|
dayMousedown: function(ev) { |
4108
|
|
|
var _this = this; |
4109
|
|
|
var view = this.view; |
4110
|
|
|
var isSelectable = view.opt('selectable'); |
4111
|
|
|
var dates = null; // the inclusive dates of the selection. will be null if no selection |
4112
|
|
|
var start; // the inclusive start of the selection |
4113
|
|
|
var end; // the *exclusive* end of the selection |
4114
|
|
|
var dayEl; |
4115
|
|
|
|
4116
|
|
|
// this listener tracks a mousedown on a day element, and a subsequent drag. |
4117
|
|
|
// if the drag ends on the same day, it is a 'dayClick'. |
4118
|
|
|
// if 'selectable' is enabled, this listener also detects selections. |
4119
|
|
|
var dragListener = new DragListener(this.coordMap, { |
4120
|
|
|
//distance: 5, // needs more work if we want dayClick to fire correctly |
4121
|
|
|
scroll: view.opt('dragScroll'), |
4122
|
|
|
dragStart: function() { |
4123
|
|
|
view.unselect(); // since we could be rendering a new selection, we want to clear any old one |
4124
|
|
|
}, |
4125
|
|
|
cellOver: function(cell, date) { |
4126
|
|
|
if (dragListener.origDate) { // click needs to have started on a cell |
4127
|
|
|
|
4128
|
|
|
dayEl = _this.getCellDayEl(cell); |
4129
|
|
|
|
4130
|
|
|
dates = [ date, dragListener.origDate ].sort(dateCompare); |
4131
|
|
|
start = dates[0]; |
4132
|
|
|
end = dates[1].clone().add(_this.cellDuration); |
4133
|
|
|
|
4134
|
|
|
if (isSelectable) { |
4135
|
|
|
_this.renderSelection(start, end); |
4136
|
|
|
} |
4137
|
|
|
} |
4138
|
|
|
}, |
4139
|
|
|
cellOut: function(cell, date) { |
4140
|
|
|
dates = null; |
4141
|
|
|
_this.destroySelection(); |
4142
|
|
|
}, |
4143
|
|
|
listenStop: function(ev) { |
4144
|
|
|
if (dates) { // started and ended on a cell? |
4145
|
|
|
if (dates[0].isSame(dates[1])) { |
4146
|
|
|
view.trigger('dayClick', dayEl[0], start, ev); |
4147
|
|
|
} |
4148
|
|
|
if (isSelectable) { |
4149
|
|
|
// the selection will already have been rendered. just report it |
4150
|
|
|
view.reportSelection(start, end, ev); |
4151
|
|
|
} |
4152
|
|
|
} |
4153
|
|
|
} |
4154
|
|
|
}); |
4155
|
|
|
|
4156
|
|
|
dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart |
4157
|
|
|
}, |
4158
|
|
|
|
4159
|
|
|
|
4160
|
|
|
/* Event Dragging |
4161
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4162
|
|
|
|
4163
|
|
|
|
4164
|
|
|
// Renders a visual indication of a event being dragged over the given date(s). |
4165
|
|
|
// `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info. |
4166
|
|
|
// A returned value of `true` signals that a mock "helper" event has been rendered. |
4167
|
|
|
renderDrag: function(start, end, seg) { |
4168
|
|
|
// subclasses must implement |
4169
|
|
|
}, |
4170
|
|
|
|
4171
|
|
|
|
4172
|
|
|
// Unrenders a visual indication of an event being dragged |
4173
|
|
|
destroyDrag: function() { |
4174
|
|
|
// subclasses must implement |
4175
|
|
|
}, |
4176
|
|
|
|
4177
|
|
|
|
4178
|
|
|
/* Event Resizing |
4179
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4180
|
|
|
|
4181
|
|
|
|
4182
|
|
|
// Renders a visual indication of an event being resized. |
4183
|
|
|
// `start` and `end` are the updated dates of the event. `seg` is the original segment object involved in the drag. |
4184
|
|
|
renderResize: function(start, end, seg) { |
4185
|
|
|
// subclasses must implement |
4186
|
|
|
}, |
4187
|
|
|
|
4188
|
|
|
|
4189
|
|
|
// Unrenders a visual indication of an event being resized. |
4190
|
|
|
destroyResize: function() { |
4191
|
|
|
// subclasses must implement |
4192
|
|
|
}, |
4193
|
|
|
|
4194
|
|
|
|
4195
|
|
|
/* Event Helper |
4196
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4197
|
|
|
|
4198
|
|
|
|
4199
|
|
|
// Renders a mock event over the given date(s). |
4200
|
|
|
// `end` can be null, in which case the mock event that is rendered will have a null end time. |
4201
|
|
|
// `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging. |
4202
|
|
|
renderRangeHelper: function(start, end, sourceSeg) { |
4203
|
|
|
var view = this.view; |
4204
|
|
|
var fakeEvent; |
4205
|
|
|
|
4206
|
|
|
// compute the end time if forced to do so (this is what EventManager does) |
4207
|
|
|
if (!end && view.opt('forceEventDuration')) { |
4208
|
|
|
end = view.calendar.getDefaultEventEnd(!start.hasTime(), start); |
4209
|
|
|
} |
4210
|
|
|
|
4211
|
|
|
fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible |
4212
|
|
|
fakeEvent.start = start; |
4213
|
|
|
fakeEvent.end = end; |
4214
|
|
|
fakeEvent.allDay = !(start.hasTime() || (end && end.hasTime())); // freshly compute allDay |
4215
|
|
|
|
4216
|
|
|
// this extra className will be useful for differentiating real events from mock events in CSS |
4217
|
|
|
fakeEvent.className = (fakeEvent.className || []).concat('fc-helper'); |
4218
|
|
|
|
4219
|
|
|
// if something external is being dragged in, don't render a resizer |
4220
|
|
|
if (!sourceSeg) { |
4221
|
|
|
fakeEvent.editable = false; |
4222
|
|
|
} |
4223
|
|
|
|
4224
|
|
|
this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering |
4225
|
|
|
}, |
4226
|
|
|
|
4227
|
|
|
|
4228
|
|
|
// Renders a mock event |
4229
|
|
|
renderHelper: function(event, sourceSeg) { |
4230
|
|
|
// subclasses must implement |
4231
|
|
|
}, |
4232
|
|
|
|
4233
|
|
|
|
4234
|
|
|
// Unrenders a mock event |
4235
|
|
|
destroyHelper: function() { |
4236
|
|
|
// subclasses must implement |
4237
|
|
|
}, |
4238
|
|
|
|
4239
|
|
|
|
4240
|
|
|
/* Selection |
4241
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4242
|
|
|
|
4243
|
|
|
|
4244
|
|
|
// Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. |
4245
|
|
|
renderSelection: function(start, end) { |
4246
|
|
|
this.renderHighlight(start, end); |
4247
|
|
|
}, |
4248
|
|
|
|
4249
|
|
|
|
4250
|
|
|
// Unrenders any visual indications of a selection. Will unrender a highlight by default. |
4251
|
|
|
destroySelection: function() { |
4252
|
|
|
this.destroyHighlight(); |
4253
|
|
|
}, |
4254
|
|
|
|
4255
|
|
|
|
4256
|
|
|
/* Highlight |
4257
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4258
|
|
|
|
4259
|
|
|
|
4260
|
|
|
// Puts visual emphasis on a certain date range |
4261
|
|
|
renderHighlight: function(start, end) { |
4262
|
|
|
// subclasses should implement |
4263
|
|
|
}, |
4264
|
|
|
|
4265
|
|
|
|
4266
|
|
|
// Removes visual emphasis on a date range |
4267
|
|
|
destroyHighlight: function() { |
4268
|
|
|
// subclasses should implement |
4269
|
|
|
}, |
4270
|
|
|
|
4271
|
|
|
|
4272
|
|
|
|
4273
|
|
|
/* Generic rendering utilities for subclasses |
4274
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4275
|
|
|
|
4276
|
|
|
|
4277
|
|
|
// Renders a day-of-week header row |
4278
|
|
|
headHtml: function() { |
4279
|
|
|
return '' + |
4280
|
|
|
'<div class="fc-row ' + this.view.widgetHeaderClass + '">' + |
4281
|
|
|
'<table>' + |
4282
|
|
|
'<thead>' + |
4283
|
|
|
this.rowHtml('head') + // leverages RowRenderer |
4284
|
|
|
'</thead>' + |
4285
|
|
|
'</table>' + |
4286
|
|
|
'</div>'; |
4287
|
|
|
}, |
4288
|
|
|
|
4289
|
|
|
|
4290
|
|
|
// Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell |
4291
|
|
|
headCellHtml: function(row, col, date) { |
4292
|
|
|
var view = this.view; |
4293
|
|
|
var calendar = view.calendar; |
4294
|
|
|
var colFormat = view.opt('columnFormat'); |
4295
|
|
|
|
4296
|
|
|
return '' + |
4297
|
|
|
'<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '">' + |
4298
|
|
|
htmlEscape(calendar.formatDate(date, colFormat)) + |
4299
|
|
|
'</th>'; |
4300
|
|
|
}, |
4301
|
|
|
|
4302
|
|
|
|
4303
|
|
|
// Renders the HTML for a single-day background cell |
4304
|
|
|
bgCellHtml: function(row, col, date) { |
4305
|
|
|
var view = this.view; |
4306
|
|
|
var classes = this.getDayClasses(date); |
4307
|
|
|
|
4308
|
|
|
classes.unshift('fc-day', view.widgetContentClass); |
4309
|
|
|
|
4310
|
|
|
return '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '"></td>'; |
4311
|
|
|
}, |
4312
|
|
|
|
4313
|
|
|
|
4314
|
|
|
// Computes HTML classNames for a single-day cell |
4315
|
|
|
getDayClasses: function(date) { |
4316
|
|
|
var view = this.view; |
4317
|
|
|
var today = view.calendar.getNow().stripTime(); |
4318
|
|
|
var classes = [ 'fc-' + dayIDs[date.day()] ]; |
4319
|
|
|
|
4320
|
|
|
if ( |
4321
|
|
|
view.name === 'month' && |
4322
|
|
|
date.month() != view.intervalStart.month() |
4323
|
|
|
) { |
4324
|
|
|
classes.push('fc-other-month'); |
4325
|
|
|
} |
4326
|
|
|
|
4327
|
|
|
if (date.isSame(today, 'day')) { |
4328
|
|
|
classes.push( |
4329
|
|
|
'fc-today', |
4330
|
|
|
view.highlightStateClass |
4331
|
|
|
); |
4332
|
|
|
} |
4333
|
|
|
else if (date < today) { |
4334
|
|
|
classes.push('fc-past'); |
4335
|
|
|
} |
4336
|
|
|
else { |
4337
|
|
|
classes.push('fc-future'); |
4338
|
|
|
} |
4339
|
|
|
|
4340
|
|
|
return classes; |
4341
|
|
|
} |
4342
|
|
|
|
4343
|
|
|
}); |
4344
|
|
|
|
4345
|
|
|
;; |
4346
|
|
|
|
4347
|
|
|
/* Event-rendering and event-interaction methods for the abstract Grid class |
4348
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
4349
|
|
|
|
4350
|
|
|
$.extend(Grid.prototype, { |
4351
|
|
|
|
4352
|
|
|
mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing |
4353
|
|
|
isDraggingSeg: false, // is a segment being dragged? boolean |
4354
|
|
|
isResizingSeg: false, // is a segment being resized? boolean |
4355
|
|
|
|
4356
|
|
|
|
4357
|
|
|
// Renders the given events onto the grid |
4358
|
|
|
renderEvents: function(events) { |
4359
|
|
|
// subclasses must implement |
4360
|
|
|
}, |
4361
|
|
|
|
4362
|
|
|
|
4363
|
|
|
// Retrieves all rendered segment objects in this grid |
4364
|
|
|
getSegs: function() { |
4365
|
|
|
// subclasses must implement |
4366
|
|
|
}, |
4367
|
|
|
|
4368
|
|
|
|
4369
|
|
|
// Unrenders all events. Subclasses should implement, calling this super-method first. |
4370
|
|
|
destroyEvents: function() { |
4371
|
|
|
this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event |
4372
|
|
|
}, |
4373
|
|
|
|
4374
|
|
|
|
4375
|
|
|
// Renders a `el` property for each seg, and only returns segments that successfully rendered |
4376
|
|
|
renderSegs: function(segs, disableResizing) { |
4377
|
|
|
var view = this.view; |
4378
|
|
|
var html = ''; |
4379
|
|
|
var renderedSegs = []; |
4380
|
|
|
var i; |
4381
|
|
|
|
4382
|
|
|
// build a large concatenation of event segment HTML |
4383
|
|
|
for (i = 0; i < segs.length; i++) { |
4384
|
|
|
html += this.renderSegHtml(segs[i], disableResizing); |
4385
|
|
|
} |
4386
|
|
|
|
4387
|
|
|
// Grab individual elements from the combined HTML string. Use each as the default rendering. |
4388
|
|
|
// Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false. |
4389
|
|
|
$(html).each(function(i, node) { |
4390
|
|
|
var seg = segs[i]; |
4391
|
|
|
var el = view.resolveEventEl(seg.event, $(node)); |
4392
|
|
|
if (el) { |
4393
|
|
|
el.data('fc-seg', seg); // used by handlers |
4394
|
|
|
seg.el = el; |
4395
|
|
|
renderedSegs.push(seg); |
4396
|
|
|
} |
4397
|
|
|
}); |
4398
|
|
|
|
4399
|
|
|
return renderedSegs; |
4400
|
|
|
}, |
4401
|
|
|
|
4402
|
|
|
|
4403
|
|
|
// Generates the HTML for the default rendering of a segment |
4404
|
|
|
renderSegHtml: function(seg, disableResizing) { |
4405
|
|
|
// subclasses must implement |
4406
|
|
|
}, |
4407
|
|
|
|
4408
|
|
|
|
4409
|
|
|
// Converts an array of event objects into an array of segment objects |
4410
|
|
|
eventsToSegs: function(events, intervalStart, intervalEnd) { |
4411
|
|
|
var _this = this; |
4412
|
|
|
|
4413
|
|
|
return $.map(events, function(event) { |
4414
|
|
|
return _this.eventToSegs(event, intervalStart, intervalEnd); // $.map flattens all returned arrays together |
4415
|
|
|
}); |
4416
|
|
|
}, |
4417
|
|
|
|
4418
|
|
|
|
4419
|
|
|
// Slices a single event into an array of event segments. |
4420
|
|
|
// When `intervalStart` and `intervalEnd` are specified, intersect the events with that interval. |
4421
|
|
|
// Otherwise, let the subclass decide how it wants to slice the segments over the grid. |
4422
|
|
|
eventToSegs: function(event, intervalStart, intervalEnd) { |
4423
|
|
|
var eventStart = event.start.clone().stripZone(); // normalize |
4424
|
|
|
var eventEnd = this.view.calendar.getEventEnd(event).stripZone(); // compute (if necessary) and normalize |
4425
|
|
|
var segs; |
4426
|
|
|
var i, seg; |
4427
|
|
|
|
4428
|
|
|
if (intervalStart && intervalEnd) { |
4429
|
|
|
seg = intersectionToSeg(eventStart, eventEnd, intervalStart, intervalEnd); |
4430
|
|
|
segs = seg ? [ seg ] : []; |
4431
|
|
|
} |
4432
|
|
|
else { |
4433
|
|
|
segs = this.rangeToSegs(eventStart, eventEnd); // defined by the subclass |
4434
|
|
|
} |
4435
|
|
|
|
4436
|
|
|
// assign extra event-related properties to the segment objects |
4437
|
|
|
for (i = 0; i < segs.length; i++) { |
4438
|
|
|
seg = segs[i]; |
4439
|
|
|
seg.event = event; |
4440
|
|
|
seg.eventStartMS = +eventStart; |
4441
|
|
|
seg.eventDurationMS = eventEnd - eventStart; |
4442
|
|
|
} |
4443
|
|
|
|
4444
|
|
|
return segs; |
4445
|
|
|
}, |
4446
|
|
|
|
4447
|
|
|
|
4448
|
|
|
/* Handlers |
4449
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4450
|
|
|
|
4451
|
|
|
|
4452
|
|
|
// Attaches event-element-related handlers to the container element and leverage bubbling |
4453
|
|
|
bindSegHandlers: function() { |
4454
|
|
|
var _this = this; |
4455
|
|
|
var view = this.view; |
4456
|
|
|
|
4457
|
|
|
$.each( |
4458
|
|
|
{ |
4459
|
|
|
mouseenter: function(seg, ev) { |
4460
|
|
|
_this.triggerSegMouseover(seg, ev); |
4461
|
|
|
}, |
4462
|
|
|
mouseleave: function(seg, ev) { |
4463
|
|
|
_this.triggerSegMouseout(seg, ev); |
4464
|
|
|
}, |
4465
|
|
|
click: function(seg, ev) { |
4466
|
|
|
return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel |
4467
|
|
|
}, |
4468
|
|
|
mousedown: function(seg, ev) { |
4469
|
|
|
if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) { |
4470
|
|
|
_this.segResizeMousedown(seg, ev); |
4471
|
|
|
} |
4472
|
|
|
else if (view.isEventDraggable(seg.event)) { |
4473
|
|
|
_this.segDragMousedown(seg, ev); |
4474
|
|
|
} |
4475
|
|
|
} |
4476
|
|
|
}, |
4477
|
|
|
function(name, func) { |
4478
|
|
|
// attach the handler to the container element and only listen for real event elements via bubbling |
4479
|
|
|
_this.el.on(name, '.fc-event-container > *', function(ev) { |
4480
|
|
|
var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents |
4481
|
|
|
|
4482
|
|
|
// only call the handlers if there is not a drag/resize in progress |
4483
|
|
|
if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) { |
4484
|
|
|
return func.call(this, seg, ev); // `this` will be the event element |
4485
|
|
|
} |
4486
|
|
|
}); |
4487
|
|
|
} |
4488
|
|
|
); |
4489
|
|
|
}, |
4490
|
|
|
|
4491
|
|
|
|
4492
|
|
|
// Updates internal state and triggers handlers for when an event element is moused over |
4493
|
|
|
triggerSegMouseover: function(seg, ev) { |
4494
|
|
|
if (!this.mousedOverSeg) { |
4495
|
|
|
this.mousedOverSeg = seg; |
4496
|
|
|
this.view.trigger('eventMouseover', seg.el[0], seg.event, ev); |
4497
|
|
|
} |
4498
|
|
|
}, |
4499
|
|
|
|
4500
|
|
|
|
4501
|
|
|
// Updates internal state and triggers handlers for when an event element is moused out. |
4502
|
|
|
// Can be given no arguments, in which case it will mouseout the segment that was previously moused over. |
4503
|
|
|
triggerSegMouseout: function(seg, ev) { |
4504
|
|
|
ev = ev || {}; // if given no args, make a mock mouse event |
4505
|
|
|
|
4506
|
|
|
if (this.mousedOverSeg) { |
4507
|
|
|
seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment |
4508
|
|
|
this.mousedOverSeg = null; |
4509
|
|
|
this.view.trigger('eventMouseout', seg.el[0], seg.event, ev); |
4510
|
|
|
} |
4511
|
|
|
}, |
4512
|
|
|
|
4513
|
|
|
|
4514
|
|
|
/* Dragging |
4515
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4516
|
|
|
|
4517
|
|
|
|
4518
|
|
|
// Called when the user does a mousedown on an event, which might lead to dragging. |
4519
|
|
|
// Generic enough to work with any type of Grid. |
4520
|
|
|
segDragMousedown: function(seg, ev) { |
4521
|
|
|
var _this = this; |
4522
|
|
|
var view = this.view; |
4523
|
|
|
var el = seg.el; |
4524
|
|
|
var event = seg.event; |
4525
|
|
|
var newStart, newEnd; |
4526
|
|
|
|
4527
|
|
|
// A clone of the original element that will move with the mouse |
4528
|
|
|
var mouseFollower = new MouseFollower(seg.el, { |
4529
|
|
|
parentEl: view.el, |
4530
|
|
|
opacity: view.opt('dragOpacity'), |
4531
|
|
|
revertDuration: view.opt('dragRevertDuration'), |
4532
|
|
|
zIndex: 2 // one above the .fc-view |
4533
|
|
|
}); |
4534
|
|
|
|
4535
|
|
|
// Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents |
4536
|
|
|
// of the view. |
4537
|
|
|
var dragListener = new DragListener(view.coordMap, { |
4538
|
|
|
distance: 5, |
4539
|
|
|
scroll: view.opt('dragScroll'), |
4540
|
|
|
listenStart: function(ev) { |
4541
|
|
|
mouseFollower.hide(); // don't show until we know this is a real drag |
4542
|
|
|
mouseFollower.start(ev); |
4543
|
|
|
}, |
4544
|
|
|
dragStart: function(ev) { |
4545
|
|
|
_this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported |
4546
|
|
|
_this.isDraggingSeg = true; |
4547
|
|
|
view.hideEvent(event); // hide all event segments. our mouseFollower will take over |
4548
|
|
|
view.trigger('eventDragStart', el[0], event, ev, {}); // last argument is jqui dummy |
4549
|
|
|
}, |
4550
|
|
|
cellOver: function(cell, date) { |
4551
|
|
|
var origDate = seg.cellDate || dragListener.origDate; |
4552
|
|
|
var res = _this.computeDraggedEventDates(seg, origDate, date); |
4553
|
|
|
newStart = res.start; |
4554
|
|
|
newEnd = res.end; |
4555
|
|
|
|
4556
|
|
|
if (view.renderDrag(newStart, newEnd, seg)) { // have the view render a visual indication |
4557
|
|
|
mouseFollower.hide(); // if the view is already using a mock event "helper", hide our own |
4558
|
|
|
} |
4559
|
|
|
else { |
4560
|
|
|
mouseFollower.show(); |
4561
|
|
|
} |
4562
|
|
|
}, |
4563
|
|
|
cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells |
4564
|
|
|
newStart = null; |
4565
|
|
|
view.destroyDrag(); // unrender whatever was done in view.renderDrag |
4566
|
|
|
mouseFollower.show(); // show in case we are moving out of all cells |
4567
|
|
|
}, |
4568
|
|
|
dragStop: function(ev) { |
4569
|
|
|
var hasChanged = newStart && !newStart.isSame(event.start); |
4570
|
|
|
|
4571
|
|
|
// do revert animation if hasn't changed. calls a callback when finished (whether animation or not) |
4572
|
|
|
mouseFollower.stop(!hasChanged, function() { |
4573
|
|
|
_this.isDraggingSeg = false; |
4574
|
|
|
view.destroyDrag(); |
4575
|
|
|
view.showEvent(event); |
4576
|
|
|
view.trigger('eventDragStop', el[0], event, ev, {}); // last argument is jqui dummy |
4577
|
|
|
|
4578
|
|
|
if (hasChanged) { |
4579
|
|
|
view.eventDrop(el[0], event, newStart, ev); // will rerender all events... |
|
|
|
|
4580
|
|
|
} |
4581
|
|
|
}); |
4582
|
|
|
}, |
4583
|
|
|
listenStop: function() { |
4584
|
|
|
mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started |
4585
|
|
|
} |
4586
|
|
|
}); |
4587
|
|
|
|
4588
|
|
|
dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart |
4589
|
|
|
}, |
4590
|
|
|
|
4591
|
|
|
|
4592
|
|
|
// Given a segment, the dates where a drag began and ended, calculates the Event Object's new start and end dates |
4593
|
|
|
computeDraggedEventDates: function(seg, dragStartDate, dropDate) { |
4594
|
|
|
var view = this.view; |
4595
|
|
|
var event = seg.event; |
4596
|
|
|
var start = event.start; |
4597
|
|
|
var end = view.calendar.getEventEnd(event); |
4598
|
|
|
var delta; |
4599
|
|
|
var newStart; |
4600
|
|
|
var newEnd; |
4601
|
|
|
|
4602
|
|
|
if (dropDate.hasTime() === dragStartDate.hasTime()) { |
4603
|
|
|
delta = dayishDiff(dropDate, dragStartDate); |
4604
|
|
|
newStart = start.clone().add(delta); |
4605
|
|
|
if (event.end === null) { // do we need to compute an end? |
4606
|
|
|
newEnd = null; |
4607
|
|
|
} |
4608
|
|
|
else { |
4609
|
|
|
newEnd = end.clone().add(delta); |
4610
|
|
|
} |
4611
|
|
|
} |
4612
|
|
|
else { |
4613
|
|
|
// if switching from day <-> timed, start should be reset to the dropped date, and the end cleared |
4614
|
|
|
newStart = dropDate; |
4615
|
|
|
newEnd = null; // end should be cleared |
4616
|
|
|
} |
4617
|
|
|
|
4618
|
|
|
return { start: newStart, end: newEnd }; |
4619
|
|
|
}, |
4620
|
|
|
|
4621
|
|
|
|
4622
|
|
|
/* Resizing |
4623
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4624
|
|
|
|
4625
|
|
|
|
4626
|
|
|
// Called when the user does a mousedown on an event's resizer, which might lead to resizing. |
4627
|
|
|
// Generic enough to work with any type of Grid. |
4628
|
|
|
segResizeMousedown: function(seg, ev) { |
4629
|
|
|
var _this = this; |
4630
|
|
|
var view = this.view; |
4631
|
|
|
var el = seg.el; |
4632
|
|
|
var event = seg.event; |
4633
|
|
|
var start = event.start; |
4634
|
|
|
var end = view.calendar.getEventEnd(event); |
4635
|
|
|
var newEnd = null; |
4636
|
|
|
var dragListener; |
4637
|
|
|
|
4638
|
|
|
function destroy() { // resets the rendering |
4639
|
|
|
_this.destroyResize(); |
4640
|
|
|
view.showEvent(event); |
4641
|
|
|
} |
4642
|
|
|
|
4643
|
|
|
// Tracks mouse movement over the *grid's* coordinate map |
4644
|
|
|
dragListener = new DragListener(this.coordMap, { |
4645
|
|
|
distance: 5, |
4646
|
|
|
scroll: view.opt('dragScroll'), |
4647
|
|
|
dragStart: function(ev) { |
4648
|
|
|
_this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported |
4649
|
|
|
_this.isResizingSeg = true; |
4650
|
|
|
view.trigger('eventResizeStart', el[0], event, ev, {}); // last argument is jqui dummy |
4651
|
|
|
}, |
4652
|
|
|
cellOver: function(cell, date) { |
4653
|
|
|
// compute the new end. don't allow it to go before the event's start |
4654
|
|
|
if (date.isBefore(start)) { // allows comparing ambig to non-ambig |
4655
|
|
|
date = start; |
4656
|
|
|
} |
4657
|
|
|
newEnd = date.clone().add(_this.cellDuration); // make it an exclusive end |
4658
|
|
|
|
4659
|
|
|
if (newEnd.isSame(end)) { |
4660
|
|
|
newEnd = null; |
4661
|
|
|
destroy(); |
4662
|
|
|
} |
4663
|
|
|
else { |
4664
|
|
|
_this.renderResize(start, newEnd, seg); |
4665
|
|
|
view.hideEvent(event); |
4666
|
|
|
} |
4667
|
|
|
}, |
4668
|
|
|
cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells |
4669
|
|
|
newEnd = null; |
4670
|
|
|
destroy(); |
4671
|
|
|
}, |
4672
|
|
|
dragStop: function(ev) { |
4673
|
|
|
_this.isResizingSeg = false; |
4674
|
|
|
destroy(); |
4675
|
|
|
view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy |
4676
|
|
|
|
4677
|
|
|
if (newEnd) { |
4678
|
|
|
view.eventResize(el[0], event, newEnd, ev); // will rerender all events... |
4679
|
|
|
} |
4680
|
|
|
} |
4681
|
|
|
}); |
4682
|
|
|
|
4683
|
|
|
dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart |
4684
|
|
|
}, |
4685
|
|
|
|
4686
|
|
|
|
4687
|
|
|
/* Rendering Utils |
4688
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4689
|
|
|
|
4690
|
|
|
|
4691
|
|
|
// Generic utility for generating the HTML classNames for an event segment's element |
4692
|
|
|
getSegClasses: function(seg, isDraggable, isResizable) { |
4693
|
|
|
var event = seg.event; |
4694
|
|
|
var classes = [ |
4695
|
|
|
'fc-event', |
4696
|
|
|
seg.isStart ? 'fc-start' : 'fc-not-start', |
4697
|
|
|
seg.isEnd ? 'fc-end' : 'fc-not-end' |
4698
|
|
|
].concat( |
4699
|
|
|
event.className, |
4700
|
|
|
event.source ? event.source.className : [] |
4701
|
|
|
); |
4702
|
|
|
|
4703
|
|
|
if (isDraggable) { |
4704
|
|
|
classes.push('fc-draggable'); |
4705
|
|
|
} |
4706
|
|
|
if (isResizable) { |
4707
|
|
|
classes.push('fc-resizable'); |
4708
|
|
|
} |
4709
|
|
|
|
4710
|
|
|
return classes; |
4711
|
|
|
}, |
4712
|
|
|
|
4713
|
|
|
|
4714
|
|
|
// Utility for generating a CSS string with all the event skin-related properties |
4715
|
|
|
getEventSkinCss: function(event) { |
4716
|
|
|
var view = this.view; |
4717
|
|
|
var source = event.source || {}; |
4718
|
|
|
var eventColor = event.color; |
4719
|
|
|
var sourceColor = source.color; |
4720
|
|
|
var optionColor = view.opt('eventColor'); |
4721
|
|
|
var backgroundColor = |
4722
|
|
|
event.backgroundColor || |
4723
|
|
|
eventColor || |
4724
|
|
|
source.backgroundColor || |
4725
|
|
|
sourceColor || |
4726
|
|
|
view.opt('eventBackgroundColor') || |
4727
|
|
|
optionColor; |
4728
|
|
|
var borderColor = |
4729
|
|
|
event.borderColor || |
4730
|
|
|
eventColor || |
4731
|
|
|
source.borderColor || |
4732
|
|
|
sourceColor || |
4733
|
|
|
view.opt('eventBorderColor') || |
4734
|
|
|
optionColor; |
4735
|
|
|
var textColor = |
4736
|
|
|
event.textColor || |
4737
|
|
|
source.textColor || |
4738
|
|
|
view.opt('eventTextColor'); |
4739
|
|
|
var statements = []; |
4740
|
|
|
if (backgroundColor) { |
4741
|
|
|
statements.push('background-color:' + backgroundColor); |
4742
|
|
|
} |
4743
|
|
|
if (borderColor) { |
4744
|
|
|
statements.push('border-color:' + borderColor); |
4745
|
|
|
} |
4746
|
|
|
if (textColor) { |
4747
|
|
|
statements.push('color:' + textColor); |
4748
|
|
|
} |
4749
|
|
|
return statements.join(';'); |
4750
|
|
|
} |
4751
|
|
|
|
4752
|
|
|
}); |
4753
|
|
|
|
4754
|
|
|
|
4755
|
|
|
/* Event Segment Utilities |
4756
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
4757
|
|
|
|
4758
|
|
|
|
4759
|
|
|
// A cmp function for determining which segments should take visual priority |
4760
|
|
|
function compareSegs(seg1, seg2) { |
4761
|
|
|
return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first |
4762
|
|
|
seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first |
4763
|
|
|
seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) |
4764
|
|
|
(seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title |
4765
|
|
|
} |
4766
|
|
|
|
4767
|
|
|
|
4768
|
|
|
;; |
4769
|
|
|
|
4770
|
|
|
/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. |
4771
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
4772
|
|
|
|
4773
|
|
|
function DayGrid(view) { |
4774
|
|
|
Grid.call(this, view); // call the super-constructor |
4775
|
|
|
} |
4776
|
|
|
|
4777
|
|
|
|
4778
|
|
|
DayGrid.prototype = createObject(Grid.prototype); // declare the super-class |
4779
|
|
|
$.extend(DayGrid.prototype, { |
4780
|
|
|
|
4781
|
|
|
numbersVisible: false, // should render a row for day/week numbers? manually set by the view |
4782
|
|
|
cellDuration: moment.duration({ days: 1 }), // required for Grid.event.js. Each cell is always a single day |
4783
|
|
|
bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid |
4784
|
|
|
|
4785
|
|
|
rowEls: null, // set of fake row elements |
4786
|
|
|
dayEls: null, // set of whole-day elements comprising the row's background |
4787
|
|
|
helperEls: null, // set of cell skeleton elements for rendering the mock event "helper" |
4788
|
|
|
highlightEls: null, // set of cell skeleton elements for rendering the highlight |
4789
|
|
|
|
4790
|
|
|
|
4791
|
|
|
// Renders the rows and columns into the component's `this.el`, which should already be assigned. |
4792
|
|
|
// isRigid determins whether the individual rows should ignore the contents and be a constant height. |
4793
|
|
|
// Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. |
4794
|
|
|
render: function(isRigid) { |
4795
|
|
|
var view = this.view; |
4796
|
|
|
var html = ''; |
4797
|
|
|
var row; |
4798
|
|
|
|
4799
|
|
|
for (row = 0; row < view.rowCnt; row++) { |
4800
|
|
|
html += this.dayRowHtml(row, isRigid); |
4801
|
|
|
} |
4802
|
|
|
this.el.html(html); |
4803
|
|
|
|
4804
|
|
|
this.rowEls = this.el.find('.fc-row'); |
4805
|
|
|
this.dayEls = this.el.find('.fc-day'); |
4806
|
|
|
|
4807
|
|
|
// run all the day cells through the dayRender callback |
4808
|
|
|
this.dayEls.each(function(i, node) { |
4809
|
|
|
var date = view.cellToDate(Math.floor(i / view.colCnt), i % view.colCnt); |
4810
|
|
|
view.trigger('dayRender', null, date, $(node)); |
4811
|
|
|
}); |
4812
|
|
|
|
4813
|
|
|
Grid.prototype.render.call(this); // call the super-method |
4814
|
|
|
}, |
4815
|
|
|
|
4816
|
|
|
|
4817
|
|
|
destroy: function() { |
4818
|
|
|
this.destroySegPopover(); |
4819
|
|
|
}, |
4820
|
|
|
|
4821
|
|
|
|
4822
|
|
|
// Generates the HTML for a single row. `row` is the row number. |
4823
|
|
|
dayRowHtml: function(row, isRigid) { |
4824
|
|
|
var view = this.view; |
4825
|
|
|
var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ]; |
4826
|
|
|
|
4827
|
|
|
if (isRigid) { |
4828
|
|
|
classes.push('fc-rigid'); |
4829
|
|
|
} |
4830
|
|
|
|
4831
|
|
|
return '' + |
4832
|
|
|
'<div class="' + classes.join(' ') + '">' + |
4833
|
|
|
'<div class="fc-bg">' + |
4834
|
|
|
'<table>' + |
4835
|
|
|
this.rowHtml('day', row) + // leverages RowRenderer. calls dayCellHtml() |
4836
|
|
|
'</table>' + |
4837
|
|
|
'</div>' + |
4838
|
|
|
'<div class="fc-content-skeleton">' + |
4839
|
|
|
'<table>' + |
4840
|
|
|
(this.numbersVisible ? |
4841
|
|
|
'<thead>' + |
4842
|
|
|
this.rowHtml('number', row) + // leverages RowRenderer. View will define render method |
4843
|
|
|
'</thead>' : |
4844
|
|
|
'' |
4845
|
|
|
) + |
4846
|
|
|
'</table>' + |
4847
|
|
|
'</div>' + |
4848
|
|
|
'</div>'; |
4849
|
|
|
}, |
4850
|
|
|
|
4851
|
|
|
|
4852
|
|
|
// Renders the HTML for a whole-day cell. Will eventually end up in the day-row's background. |
4853
|
|
|
// We go through a 'day' row type instead of just doing a 'bg' row type so that the View can do custom rendering |
4854
|
|
|
// specifically for whole-day rows, whereas a 'bg' might also be used for other purposes (TimeGrid bg for example). |
4855
|
|
|
dayCellHtml: function(row, col, date) { |
4856
|
|
|
return this.bgCellHtml(row, col, date); |
4857
|
|
|
}, |
4858
|
|
|
|
4859
|
|
|
|
4860
|
|
|
/* Coordinates & Cells |
4861
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4862
|
|
|
|
4863
|
|
|
|
4864
|
|
|
// Populates the empty `rows` and `cols` arrays with coordinates of the cells. For CoordGrid. |
4865
|
|
|
buildCoords: function(rows, cols) { |
4866
|
|
|
var colCnt = this.view.colCnt; |
4867
|
|
|
var e, n, p; |
4868
|
|
|
|
4869
|
|
|
this.dayEls.slice(0, colCnt).each(function(i, _e) { // iterate the first row of day elements |
4870
|
|
|
e = $(_e); |
4871
|
|
|
n = e.offset().left; |
4872
|
|
|
if (i) { |
4873
|
|
|
p[1] = n; |
4874
|
|
|
} |
4875
|
|
|
p = [ n ]; |
4876
|
|
|
cols[i] = p; |
4877
|
|
|
}); |
4878
|
|
|
p[1] = n + e.outerWidth(); |
4879
|
|
|
|
4880
|
|
|
this.rowEls.each(function(i, _e) { |
4881
|
|
|
e = $(_e); |
4882
|
|
|
n = e.offset().top; |
4883
|
|
|
if (i) { |
4884
|
|
|
p[1] = n; |
4885
|
|
|
} |
4886
|
|
|
p = [ n ]; |
4887
|
|
|
rows[i] = p; |
4888
|
|
|
}); |
4889
|
|
|
p[1] = n + e.outerHeight() + this.bottomCoordPadding; // hack to extend hit area of last row |
4890
|
|
|
}, |
4891
|
|
|
|
4892
|
|
|
|
4893
|
|
|
// Converts a cell to a date |
4894
|
|
|
getCellDate: function(cell) { |
4895
|
|
|
return this.view.cellToDate(cell); // leverages the View's cell system |
4896
|
|
|
}, |
4897
|
|
|
|
4898
|
|
|
|
4899
|
|
|
// Gets the whole-day element associated with the cell |
4900
|
|
|
getCellDayEl: function(cell) { |
4901
|
|
|
return this.dayEls.eq(cell.row * this.view.colCnt + cell.col); |
4902
|
|
|
}, |
4903
|
|
|
|
4904
|
|
|
|
4905
|
|
|
// Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects |
4906
|
|
|
rangeToSegs: function(start, end) { |
4907
|
|
|
return this.view.rangeToSegments(start, end); // leverages the View's cell system |
4908
|
|
|
}, |
4909
|
|
|
|
4910
|
|
|
|
4911
|
|
|
/* Event Drag Visualization |
4912
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4913
|
|
|
|
4914
|
|
|
|
4915
|
|
|
// Renders a visual indication of an event hovering over the given date(s). |
4916
|
|
|
// `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info. |
4917
|
|
|
// A returned value of `true` signals that a mock "helper" event has been rendered. |
4918
|
|
|
renderDrag: function(start, end, seg) { |
4919
|
|
|
var opacity; |
4920
|
|
|
|
4921
|
|
|
// always render a highlight underneath |
4922
|
|
|
this.renderHighlight( |
4923
|
|
|
start, |
4924
|
|
|
end || this.view.calendar.getDefaultEventEnd(true, start) |
4925
|
|
|
); |
4926
|
|
|
|
4927
|
|
|
// if a segment from the same calendar but another component is being dragged, render a helper event |
4928
|
|
|
if (seg && !seg.el.closest(this.el).length) { |
4929
|
|
|
|
4930
|
|
|
this.renderRangeHelper(start, end, seg); |
4931
|
|
|
|
4932
|
|
|
opacity = this.view.opt('dragOpacity'); |
4933
|
|
|
if (opacity !== undefined) { |
4934
|
|
|
this.helperEls.css('opacity', opacity); |
4935
|
|
|
} |
4936
|
|
|
|
4937
|
|
|
return true; // a helper has been rendered |
4938
|
|
|
} |
4939
|
|
|
}, |
4940
|
|
|
|
4941
|
|
|
|
4942
|
|
|
// Unrenders any visual indication of a hovering event |
4943
|
|
|
destroyDrag: function() { |
4944
|
|
|
this.destroyHighlight(); |
4945
|
|
|
this.destroyHelper(); |
4946
|
|
|
}, |
4947
|
|
|
|
4948
|
|
|
|
4949
|
|
|
/* Event Resize Visualization |
4950
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4951
|
|
|
|
4952
|
|
|
|
4953
|
|
|
// Renders a visual indication of an event being resized |
4954
|
|
|
renderResize: function(start, end, seg) { |
4955
|
|
|
this.renderHighlight(start, end); |
4956
|
|
|
this.renderRangeHelper(start, end, seg); |
4957
|
|
|
}, |
4958
|
|
|
|
4959
|
|
|
|
4960
|
|
|
// Unrenders a visual indication of an event being resized |
4961
|
|
|
destroyResize: function() { |
4962
|
|
|
this.destroyHighlight(); |
4963
|
|
|
this.destroyHelper(); |
4964
|
|
|
}, |
4965
|
|
|
|
4966
|
|
|
|
4967
|
|
|
/* Event Helper |
4968
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
4969
|
|
|
|
4970
|
|
|
|
4971
|
|
|
// Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. |
4972
|
|
|
renderHelper: function(event, sourceSeg) { |
4973
|
|
|
var helperNodes = []; |
4974
|
|
|
var rowStructs = this.renderEventRows([ event ]); |
4975
|
|
|
|
4976
|
|
|
// inject each new event skeleton into each associated row |
4977
|
|
|
this.rowEls.each(function(row, rowNode) { |
4978
|
|
|
var rowEl = $(rowNode); // the .fc-row |
4979
|
|
|
var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned |
4980
|
|
|
var skeletonTop; |
4981
|
|
|
|
4982
|
|
|
// If there is an original segment, match the top position. Otherwise, put it at the row's top level |
4983
|
|
|
if (sourceSeg && sourceSeg.row === row) { |
4984
|
|
|
skeletonTop = sourceSeg.el.position().top; |
4985
|
|
|
} |
4986
|
|
|
else { |
4987
|
|
|
skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top; |
4988
|
|
|
} |
4989
|
|
|
|
4990
|
|
|
skeletonEl.css('top', skeletonTop) |
4991
|
|
|
.find('table') |
4992
|
|
|
.append(rowStructs[row].tbodyEl); |
4993
|
|
|
|
4994
|
|
|
rowEl.append(skeletonEl); |
4995
|
|
|
helperNodes.push(skeletonEl[0]); |
4996
|
|
|
}); |
4997
|
|
|
|
4998
|
|
|
this.helperEls = $(helperNodes); // array -> jQuery set |
4999
|
|
|
}, |
5000
|
|
|
|
5001
|
|
|
|
5002
|
|
|
// Unrenders any visual indication of a mock helper event |
5003
|
|
|
destroyHelper: function() { |
5004
|
|
|
if (this.helperEls) { |
5005
|
|
|
this.helperEls.remove(); |
5006
|
|
|
this.helperEls = null; |
5007
|
|
|
} |
5008
|
|
|
}, |
5009
|
|
|
|
5010
|
|
|
|
5011
|
|
|
/* Highlighting |
5012
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
5013
|
|
|
|
5014
|
|
|
|
5015
|
|
|
// Renders an emphasis on the given date range. `start` is an inclusive, `end` is exclusive. |
5016
|
|
|
renderHighlight: function(start, end) { |
5017
|
|
|
var segs = this.rangeToSegs(start, end); |
5018
|
|
|
var highlightNodes = []; |
5019
|
|
|
var i, seg; |
5020
|
|
|
var el; |
5021
|
|
|
|
5022
|
|
|
// build an event skeleton for each row that needs it |
5023
|
|
|
for (i = 0; i < segs.length; i++) { |
5024
|
|
|
seg = segs[i]; |
5025
|
|
|
el = $( |
5026
|
|
|
this.highlightSkeletonHtml(seg.leftCol, seg.rightCol + 1) // make end exclusive |
5027
|
|
|
); |
5028
|
|
|
el.appendTo(this.rowEls[seg.row]); |
5029
|
|
|
highlightNodes.push(el[0]); |
5030
|
|
|
} |
5031
|
|
|
|
5032
|
|
|
this.highlightEls = $(highlightNodes); // array -> jQuery set |
5033
|
|
|
}, |
5034
|
|
|
|
5035
|
|
|
|
5036
|
|
|
// Unrenders any visual emphasis on a date range |
5037
|
|
|
destroyHighlight: function() { |
5038
|
|
|
if (this.highlightEls) { |
5039
|
|
|
this.highlightEls.remove(); |
5040
|
|
|
this.highlightEls = null; |
5041
|
|
|
} |
5042
|
|
|
}, |
5043
|
|
|
|
5044
|
|
|
|
5045
|
|
|
// Generates the HTML used to build a single-row "highlight skeleton", a table that frames highlight cells |
5046
|
|
|
highlightSkeletonHtml: function(startCol, endCol) { |
5047
|
|
|
var colCnt = this.view.colCnt; |
5048
|
|
|
var cellHtml = ''; |
5049
|
|
|
|
5050
|
|
|
if (startCol > 0) { |
5051
|
|
|
cellHtml += '<td colspan="' + startCol + '"/>'; |
5052
|
|
|
} |
5053
|
|
|
if (endCol > startCol) { |
5054
|
|
|
cellHtml += '<td colspan="' + (endCol - startCol) + '" class="fc-highlight" />'; |
5055
|
|
|
} |
5056
|
|
|
if (colCnt > endCol) { |
5057
|
|
|
cellHtml += '<td colspan="' + (colCnt - endCol) + '"/>'; |
5058
|
|
|
} |
5059
|
|
|
|
5060
|
|
|
cellHtml = this.bookendCells(cellHtml, 'highlight'); |
5061
|
|
|
|
5062
|
|
|
return '' + |
5063
|
|
|
'<div class="fc-highlight-skeleton">' + |
5064
|
|
|
'<table>' + |
5065
|
|
|
'<tr>' + |
5066
|
|
|
cellHtml + |
5067
|
|
|
'</tr>' + |
5068
|
|
|
'</table>' + |
5069
|
|
|
'</div>'; |
5070
|
|
|
} |
5071
|
|
|
|
5072
|
|
|
}); |
5073
|
|
|
|
5074
|
|
|
;; |
5075
|
|
|
|
5076
|
|
|
/* Event-rendering methods for the DayGrid class |
5077
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
5078
|
|
|
|
5079
|
|
|
$.extend(DayGrid.prototype, { |
5080
|
|
|
|
5081
|
|
|
segs: null, |
5082
|
|
|
rowStructs: null, // an array of objects, each holding information about a row's event-rendering |
5083
|
|
|
|
5084
|
|
|
|
5085
|
|
|
// Render the given events onto the Grid and return the rendered segments |
5086
|
|
|
renderEvents: function(events) { |
5087
|
|
|
var rowStructs = this.rowStructs = this.renderEventRows(events); |
5088
|
|
|
var segs = []; |
5089
|
|
|
|
5090
|
|
|
// append to each row's content skeleton |
5091
|
|
|
this.rowEls.each(function(i, rowNode) { |
5092
|
|
|
$(rowNode).find('.fc-content-skeleton > table').append( |
5093
|
|
|
rowStructs[i].tbodyEl |
5094
|
|
|
); |
5095
|
|
|
segs.push.apply(segs, rowStructs[i].segs); |
5096
|
|
|
}); |
5097
|
|
|
|
5098
|
|
|
this.segs = segs; |
5099
|
|
|
}, |
5100
|
|
|
|
5101
|
|
|
|
5102
|
|
|
// Retrieves all segment objects that have been rendered |
5103
|
|
|
getSegs: function() { |
5104
|
|
|
return (this.segs || []).concat( |
5105
|
|
|
this.popoverSegs || [] // segs rendered in the "more" events popover |
5106
|
|
|
); |
5107
|
|
|
}, |
5108
|
|
|
|
5109
|
|
|
|
5110
|
|
|
// Removes all rendered event elements |
5111
|
|
|
destroyEvents: function() { |
5112
|
|
|
var rowStructs; |
5113
|
|
|
var rowStruct; |
5114
|
|
|
|
5115
|
|
|
Grid.prototype.destroyEvents.call(this); // call the super-method |
5116
|
|
|
|
5117
|
|
|
rowStructs = this.rowStructs || []; |
5118
|
|
|
while ((rowStruct = rowStructs.pop())) { |
5119
|
|
|
rowStruct.tbodyEl.remove(); |
5120
|
|
|
} |
5121
|
|
|
|
5122
|
|
|
this.segs = null; |
5123
|
|
|
this.destroySegPopover(); // removes the "more.." events popover |
5124
|
|
|
}, |
5125
|
|
|
|
5126
|
|
|
|
5127
|
|
|
// Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton. |
5128
|
|
|
// Returns an array of rowStruct objects (see the bottom of `renderEventRow`). |
5129
|
|
|
renderEventRows: function(events) { |
5130
|
|
|
var segs = this.eventsToSegs(events); |
5131
|
|
|
var rowStructs = []; |
5132
|
|
|
var segRows; |
5133
|
|
|
var row; |
5134
|
|
|
|
5135
|
|
|
segs = this.renderSegs(segs); // returns a new array with only visible segments |
5136
|
|
|
segRows = this.groupSegRows(segs); // group into nested arrays |
5137
|
|
|
|
5138
|
|
|
// iterate each row of segment groupings |
5139
|
|
|
for (row = 0; row < segRows.length; row++) { |
5140
|
|
|
rowStructs.push( |
5141
|
|
|
this.renderEventRow(row, segRows[row]) |
5142
|
|
|
); |
5143
|
|
|
} |
5144
|
|
|
|
5145
|
|
|
return rowStructs; |
5146
|
|
|
}, |
5147
|
|
|
|
5148
|
|
|
|
5149
|
|
|
// Builds the HTML to be used for the default element for an individual segment |
5150
|
|
|
renderSegHtml: function(seg, disableResizing) { |
5151
|
|
|
var view = this.view; |
5152
|
|
|
var isRTL = view.opt('isRTL'); |
5153
|
|
|
var event = seg.event; |
5154
|
|
|
var isDraggable = view.isEventDraggable(event); |
5155
|
|
|
var isResizable = !disableResizing && event.allDay && seg.isEnd && view.isEventResizable(event); |
5156
|
|
|
var classes = this.getSegClasses(seg, isDraggable, isResizable); |
5157
|
|
|
var skinCss = this.getEventSkinCss(event); |
5158
|
|
|
var timeHtml = ''; |
5159
|
|
|
var titleHtml; |
5160
|
|
|
|
5161
|
|
|
classes.unshift('fc-day-grid-event'); |
5162
|
|
|
|
5163
|
|
|
// Only display a timed events time if it is the starting segment |
5164
|
|
|
if (!event.allDay && seg.isStart) { |
5165
|
|
|
timeHtml = '<span class="fc-time">' + htmlEscape(view.getEventTimeText(event)) + '</span>'; |
5166
|
|
|
} |
5167
|
|
|
|
5168
|
|
|
titleHtml = |
5169
|
|
|
'<span class="fc-title">' + |
5170
|
|
|
(htmlEscape(event.title || '') || ' ') + // we always want one line of height |
5171
|
|
|
'</span>'; |
5172
|
|
|
|
5173
|
|
|
return '<a class="' + classes.join(' ') + '"' + |
5174
|
|
|
(event.url ? |
5175
|
|
|
' href="' + htmlEscape(event.url) + '"' : |
5176
|
|
|
'' |
5177
|
|
|
) + |
5178
|
|
|
(skinCss ? |
5179
|
|
|
' style="' + skinCss + '"' : |
5180
|
|
|
'' |
5181
|
|
|
) + |
5182
|
|
|
'>' + |
5183
|
|
|
'<div class="fc-content">' + |
5184
|
|
|
(isRTL ? |
5185
|
|
|
titleHtml + ' ' + timeHtml : // put a natural space in between |
5186
|
|
|
timeHtml + ' ' + titleHtml // |
5187
|
|
|
) + |
5188
|
|
|
'</div>' + |
5189
|
|
|
(isResizable ? |
5190
|
|
|
'<div class="fc-resizer"/>' : |
5191
|
|
|
'' |
5192
|
|
|
) + |
5193
|
|
|
'</a>'; |
5194
|
|
|
}, |
5195
|
|
|
|
5196
|
|
|
|
5197
|
|
|
// Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains |
5198
|
|
|
// the segments. Returns object with a bunch of internal data about how the render was calculated. |
5199
|
|
|
renderEventRow: function(row, rowSegs) { |
5200
|
|
|
var view = this.view; |
5201
|
|
|
var colCnt = view.colCnt; |
5202
|
|
|
var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels |
5203
|
|
|
var levelCnt = Math.max(1, segLevels.length); // ensure at least one level |
5204
|
|
|
var tbody = $('<tbody/>'); |
5205
|
|
|
var segMatrix = []; // lookup for which segments are rendered into which level+col cells |
5206
|
|
|
var cellMatrix = []; // lookup for all <td> elements of the level+col matrix |
5207
|
|
|
var loneCellMatrix = []; // lookup for <td> elements that only take up a single column |
5208
|
|
|
var i, levelSegs; |
5209
|
|
|
var col; |
5210
|
|
|
var tr; |
5211
|
|
|
var j, seg; |
5212
|
|
|
var td; |
5213
|
|
|
|
5214
|
|
|
// populates empty cells from the current column (`col`) to `endCol` |
5215
|
|
|
function emptyCellsUntil(endCol) { |
5216
|
|
|
while (col < endCol) { |
|
|
|
|
5217
|
|
|
// try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell |
5218
|
|
|
td = (loneCellMatrix[i - 1] || [])[col]; |
5219
|
|
|
if (td) { |
|
|
|
|
5220
|
|
|
td.attr( |
5221
|
|
|
'rowspan', |
5222
|
|
|
parseInt(td.attr('rowspan') || 1, 10) + 1 |
5223
|
|
|
); |
5224
|
|
|
} |
5225
|
|
|
else { |
5226
|
|
|
td = $('<td/>'); |
5227
|
|
|
tr.append(td); |
5228
|
|
|
} |
5229
|
|
|
cellMatrix[i][col] = td; |
5230
|
|
|
loneCellMatrix[i][col] = td; |
5231
|
|
|
col++; |
5232
|
|
|
} |
5233
|
|
|
} |
5234
|
|
|
|
5235
|
|
|
for (i = 0; i < levelCnt; i++) { // iterate through all levels |
5236
|
|
|
levelSegs = segLevels[i]; |
5237
|
|
|
col = 0; |
5238
|
|
|
tr = $('<tr/>'); |
5239
|
|
|
|
5240
|
|
|
segMatrix.push([]); |
5241
|
|
|
cellMatrix.push([]); |
5242
|
|
|
loneCellMatrix.push([]); |
5243
|
|
|
|
5244
|
|
|
// levelCnt might be 1 even though there are no actual levels. protect against this. |
5245
|
|
|
// this single empty row is useful for styling. |
5246
|
|
|
if (levelSegs) { |
5247
|
|
|
for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level |
5248
|
|
|
seg = levelSegs[j]; |
5249
|
|
|
|
5250
|
|
|
emptyCellsUntil(seg.leftCol); |
5251
|
|
|
|
5252
|
|
|
// create a container that occupies or more columns. append the event element. |
5253
|
|
|
td = $('<td class="fc-event-container"/>').append(seg.el); |
5254
|
|
|
if (seg.leftCol != seg.rightCol) { |
5255
|
|
|
td.attr('colspan', seg.rightCol - seg.leftCol + 1); |
5256
|
|
|
} |
5257
|
|
|
else { // a single-column segment |
5258
|
|
|
loneCellMatrix[i][col] = td; |
5259
|
|
|
} |
5260
|
|
|
|
5261
|
|
|
while (col <= seg.rightCol) { |
5262
|
|
|
cellMatrix[i][col] = td; |
5263
|
|
|
segMatrix[i][col] = seg; |
5264
|
|
|
col++; |
5265
|
|
|
} |
5266
|
|
|
|
5267
|
|
|
tr.append(td); |
5268
|
|
|
} |
5269
|
|
|
} |
5270
|
|
|
|
5271
|
|
|
emptyCellsUntil(colCnt); // finish off the row |
5272
|
|
|
this.bookendCells(tr, 'eventSkeleton'); |
5273
|
|
|
tbody.append(tr); |
5274
|
|
|
} |
5275
|
|
|
|
5276
|
|
|
return { // a "rowStruct" |
5277
|
|
|
row: row, // the row number |
5278
|
|
|
tbodyEl: tbody, |
5279
|
|
|
cellMatrix: cellMatrix, |
5280
|
|
|
segMatrix: segMatrix, |
5281
|
|
|
segLevels: segLevels, |
5282
|
|
|
segs: rowSegs |
5283
|
|
|
}; |
5284
|
|
|
}, |
5285
|
|
|
|
5286
|
|
|
|
5287
|
|
|
// Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. |
5288
|
|
|
buildSegLevels: function(segs) { |
5289
|
|
|
var levels = []; |
5290
|
|
|
var i, seg; |
5291
|
|
|
var j; |
5292
|
|
|
|
5293
|
|
|
// Give preference to elements with certain criteria, so they have |
5294
|
|
|
// a chance to be closer to the top. |
5295
|
|
|
segs.sort(compareSegs); |
5296
|
|
|
|
5297
|
|
|
for (i = 0; i < segs.length; i++) { |
5298
|
|
|
seg = segs[i]; |
5299
|
|
|
|
5300
|
|
|
// loop through levels, starting with the topmost, until the segment doesn't collide with other segments |
5301
|
|
|
for (j = 0; j < levels.length; j++) { |
5302
|
|
|
if (!isDaySegCollision(seg, levels[j])) { |
5303
|
|
|
break; |
5304
|
|
|
} |
5305
|
|
|
} |
5306
|
|
|
// `j` now holds the desired subrow index |
5307
|
|
|
seg.level = j; |
5308
|
|
|
|
5309
|
|
|
// create new level array if needed and append segment |
5310
|
|
|
(levels[j] || (levels[j] = [])).push(seg); |
5311
|
|
|
} |
5312
|
|
|
|
5313
|
|
|
// order segments left-to-right. very important if calendar is RTL |
5314
|
|
|
for (j = 0; j < levels.length; j++) { |
5315
|
|
|
levels[j].sort(compareDaySegCols); |
5316
|
|
|
} |
5317
|
|
|
|
5318
|
|
|
return levels; |
5319
|
|
|
}, |
5320
|
|
|
|
5321
|
|
|
|
5322
|
|
|
// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row |
5323
|
|
|
groupSegRows: function(segs) { |
5324
|
|
|
var view = this.view; |
5325
|
|
|
var segRows = []; |
5326
|
|
|
var i; |
5327
|
|
|
|
5328
|
|
|
for (i = 0; i < view.rowCnt; i++) { |
5329
|
|
|
segRows.push([]); |
5330
|
|
|
} |
5331
|
|
|
|
5332
|
|
|
for (i = 0; i < segs.length; i++) { |
5333
|
|
|
segRows[segs[i].row].push(segs[i]); |
5334
|
|
|
} |
5335
|
|
|
|
5336
|
|
|
return segRows; |
5337
|
|
|
} |
5338
|
|
|
|
5339
|
|
|
}); |
5340
|
|
|
|
5341
|
|
|
|
5342
|
|
|
// Computes whether two segments' columns collide. They are assumed to be in the same row. |
5343
|
|
|
function isDaySegCollision(seg, otherSegs) { |
5344
|
|
|
var i, otherSeg; |
5345
|
|
|
|
5346
|
|
|
for (i = 0; i < otherSegs.length; i++) { |
5347
|
|
|
otherSeg = otherSegs[i]; |
5348
|
|
|
|
5349
|
|
|
if ( |
5350
|
|
|
otherSeg.leftCol <= seg.rightCol && |
5351
|
|
|
otherSeg.rightCol >= seg.leftCol |
5352
|
|
|
) { |
5353
|
|
|
return true; |
5354
|
|
|
} |
5355
|
|
|
} |
5356
|
|
|
|
5357
|
|
|
return false; |
5358
|
|
|
} |
5359
|
|
|
|
5360
|
|
|
|
5361
|
|
|
// A cmp function for determining the leftmost event |
5362
|
|
|
function compareDaySegCols(a, b) { |
5363
|
|
|
return a.leftCol - b.leftCol; |
5364
|
|
|
} |
5365
|
|
|
|
5366
|
|
|
;; |
5367
|
|
|
|
5368
|
|
|
/* Methods relate to limiting the number events for a given day on a DayGrid |
5369
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
5370
|
|
|
|
5371
|
|
|
$.extend(DayGrid.prototype, { |
5372
|
|
|
|
5373
|
|
|
|
5374
|
|
|
segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible |
5375
|
|
|
popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible |
5376
|
|
|
|
5377
|
|
|
|
5378
|
|
|
destroySegPopover: function() { |
5379
|
|
|
if (this.segPopover) { |
5380
|
|
|
this.segPopover.hide(); // will trigger destruction of `segPopover` and `popoverSegs` |
5381
|
|
|
} |
5382
|
|
|
}, |
5383
|
|
|
|
5384
|
|
|
|
5385
|
|
|
// Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. |
5386
|
|
|
// `levelLimit` can be false (don't limit), a number, or true (should be computed). |
5387
|
|
|
limitRows: function(levelLimit) { |
5388
|
|
|
var rowStructs = this.rowStructs || []; |
5389
|
|
|
var row; // row # |
5390
|
|
|
var rowLevelLimit; |
5391
|
|
|
|
5392
|
|
|
for (row = 0; row < rowStructs.length; row++) { |
5393
|
|
|
this.unlimitRow(row); |
5394
|
|
|
|
5395
|
|
|
if (!levelLimit) { |
5396
|
|
|
rowLevelLimit = false; |
5397
|
|
|
} |
5398
|
|
|
else if (typeof levelLimit === 'number') { |
5399
|
|
|
rowLevelLimit = levelLimit; |
5400
|
|
|
} |
5401
|
|
|
else { |
5402
|
|
|
rowLevelLimit = this.computeRowLevelLimit(row); |
5403
|
|
|
} |
5404
|
|
|
|
5405
|
|
|
if (rowLevelLimit !== false) { |
5406
|
|
|
this.limitRow(row, rowLevelLimit); |
5407
|
|
|
} |
5408
|
|
|
} |
5409
|
|
|
}, |
5410
|
|
|
|
5411
|
|
|
|
5412
|
|
|
// Computes the number of levels a row will accomodate without going outside its bounds. |
5413
|
|
|
// Assumes the row is "rigid" (maintains a constant height regardless of what is inside). |
5414
|
|
|
// `row` is the row number. |
5415
|
|
|
computeRowLevelLimit: function(row) { |
5416
|
|
|
var rowEl = this.rowEls.eq(row); // the containing "fake" row div |
5417
|
|
|
var rowHeight = rowEl.height(); // TODO: cache somehow? |
5418
|
|
|
var trEls = this.rowStructs[row].tbodyEl.children(); |
5419
|
|
|
var i, trEl; |
5420
|
|
|
|
5421
|
|
|
// Reveal one level <tr> at a time and stop when we find one out of bounds |
5422
|
|
|
for (i = 0; i < trEls.length; i++) { |
5423
|
|
|
trEl = trEls.eq(i).removeClass('fc-limited'); // get and reveal |
5424
|
|
|
if (trEl.position().top + trEl.outerHeight() > rowHeight) { |
5425
|
|
|
return i; |
5426
|
|
|
} |
5427
|
|
|
} |
5428
|
|
|
|
5429
|
|
|
return false; // should not limit at all |
5430
|
|
|
}, |
5431
|
|
|
|
5432
|
|
|
|
5433
|
|
|
// Limits the given grid row to the maximum number of levels and injects "more" links if necessary. |
5434
|
|
|
// `row` is the row number. |
5435
|
|
|
// `levelLimit` is a number for the maximum (inclusive) number of levels allowed. |
5436
|
|
|
limitRow: function(row, levelLimit) { |
5437
|
|
|
var _this = this; |
5438
|
|
|
var view = this.view; |
5439
|
|
|
var rowStruct = this.rowStructs[row]; |
5440
|
|
|
var moreNodes = []; // array of "more" <a> links and <td> DOM nodes |
5441
|
|
|
var col = 0; // col # |
5442
|
|
|
var cell; |
5443
|
|
|
var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right |
5444
|
|
|
var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row |
5445
|
|
|
var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes |
5446
|
|
|
var i, seg; |
5447
|
|
|
var segsBelow; // array of segment objects below `seg` in the current `col` |
5448
|
|
|
var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies |
5449
|
|
|
var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) |
5450
|
|
|
var td, rowspan; |
5451
|
|
|
var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell |
5452
|
|
|
var j; |
5453
|
|
|
var moreTd, moreWrap, moreLink; |
5454
|
|
|
|
5455
|
|
|
// Iterates through empty level cells and places "more" links inside if need be |
5456
|
|
|
function emptyCellsUntil(endCol) { // goes from current `col` to `endCol` |
5457
|
|
|
while (col < endCol) { |
|
|
|
|
5458
|
|
|
cell = { row: row, col: col }; |
5459
|
|
|
segsBelow = _this.getCellSegs(cell, levelLimit); |
|
|
|
|
5460
|
|
|
if (segsBelow.length) { |
|
|
|
|
5461
|
|
|
td = cellMatrix[levelLimit - 1][col]; |
5462
|
|
|
moreLink = _this.renderMoreLink(cell, segsBelow); |
5463
|
|
|
moreWrap = $('<div/>').append(moreLink); |
|
|
|
|
5464
|
|
|
td.append(moreWrap); |
|
|
|
|
5465
|
|
|
moreNodes.push(moreWrap[0]); |
5466
|
|
|
} |
5467
|
|
|
col++; |
5468
|
|
|
} |
5469
|
|
|
} |
5470
|
|
|
|
5471
|
|
|
if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? |
5472
|
|
|
levelSegs = rowStruct.segLevels[levelLimit - 1]; |
5473
|
|
|
cellMatrix = rowStruct.cellMatrix; |
5474
|
|
|
|
5475
|
|
|
limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit |
5476
|
|
|
.addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array |
5477
|
|
|
|
5478
|
|
|
// iterate though segments in the last allowable level |
5479
|
|
|
for (i = 0; i < levelSegs.length; i++) { |
5480
|
|
|
seg = levelSegs[i]; |
5481
|
|
|
emptyCellsUntil(seg.leftCol); // process empty cells before the segment |
5482
|
|
|
|
5483
|
|
|
// determine *all* segments below `seg` that occupy the same columns |
5484
|
|
|
colSegsBelow = []; |
5485
|
|
|
totalSegsBelow = 0; |
5486
|
|
|
while (col <= seg.rightCol) { |
5487
|
|
|
cell = { row: row, col: col }; |
5488
|
|
|
segsBelow = this.getCellSegs(cell, levelLimit); |
5489
|
|
|
colSegsBelow.push(segsBelow); |
5490
|
|
|
totalSegsBelow += segsBelow.length; |
5491
|
|
|
col++; |
5492
|
|
|
} |
5493
|
|
|
|
5494
|
|
|
if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? |
5495
|
|
|
td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell |
5496
|
|
|
rowspan = td.attr('rowspan') || 1; |
5497
|
|
|
segMoreNodes = []; |
5498
|
|
|
|
5499
|
|
|
// make a replacement <td> for each column the segment occupies. will be one for each colspan |
5500
|
|
|
for (j = 0; j < colSegsBelow.length; j++) { |
5501
|
|
|
moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan); |
5502
|
|
|
segsBelow = colSegsBelow[j]; |
5503
|
|
|
cell = { row: row, col: seg.leftCol + j }; |
5504
|
|
|
moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too |
5505
|
|
|
moreWrap = $('<div/>').append(moreLink); |
5506
|
|
|
moreTd.append(moreWrap); |
5507
|
|
|
segMoreNodes.push(moreTd[0]); |
5508
|
|
|
moreNodes.push(moreTd[0]); |
5509
|
|
|
} |
5510
|
|
|
|
5511
|
|
|
td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements |
5512
|
|
|
limitedNodes.push(td[0]); |
5513
|
|
|
} |
5514
|
|
|
} |
5515
|
|
|
|
5516
|
|
|
emptyCellsUntil(view.colCnt); // finish off the level |
5517
|
|
|
rowStruct.moreEls = $(moreNodes); // for easy undoing later |
5518
|
|
|
rowStruct.limitedEls = $(limitedNodes); // for easy undoing later |
5519
|
|
|
} |
5520
|
|
|
}, |
5521
|
|
|
|
5522
|
|
|
|
5523
|
|
|
// Reveals all levels and removes all "more"-related elements for a grid's row. |
5524
|
|
|
// `row` is a row number. |
5525
|
|
|
unlimitRow: function(row) { |
5526
|
|
|
var rowStruct = this.rowStructs[row]; |
5527
|
|
|
|
5528
|
|
|
if (rowStruct.moreEls) { |
5529
|
|
|
rowStruct.moreEls.remove(); |
5530
|
|
|
rowStruct.moreEls = null; |
5531
|
|
|
} |
5532
|
|
|
|
5533
|
|
|
if (rowStruct.limitedEls) { |
5534
|
|
|
rowStruct.limitedEls.removeClass('fc-limited'); |
5535
|
|
|
rowStruct.limitedEls = null; |
5536
|
|
|
} |
5537
|
|
|
}, |
5538
|
|
|
|
5539
|
|
|
|
5540
|
|
|
// Renders an <a> element that represents hidden event element for a cell. |
5541
|
|
|
// Responsible for attaching click handler as well. |
5542
|
|
|
renderMoreLink: function(cell, hiddenSegs) { |
5543
|
|
|
var _this = this; |
5544
|
|
|
var view = this.view; |
5545
|
|
|
|
5546
|
|
|
return $('<a class="fc-more"/>') |
5547
|
|
|
.text( |
5548
|
|
|
this.getMoreLinkText(hiddenSegs.length) |
5549
|
|
|
) |
5550
|
|
|
.on('click', function(ev) { |
5551
|
|
|
var clickOption = view.opt('eventLimitClick'); |
5552
|
|
|
var date = view.cellToDate(cell); |
5553
|
|
|
var moreEl = $(this); |
5554
|
|
|
var dayEl = _this.getCellDayEl(cell); |
5555
|
|
|
var allSegs = _this.getCellSegs(cell); |
5556
|
|
|
|
5557
|
|
|
// rescope the segments to be within the cell's date |
5558
|
|
|
var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); |
5559
|
|
|
var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); |
5560
|
|
|
|
5561
|
|
|
if (typeof clickOption === 'function') { |
5562
|
|
|
// the returned value can be an atomic option |
5563
|
|
|
clickOption = view.trigger('eventLimitClick', null, { |
5564
|
|
|
date: date, |
5565
|
|
|
dayEl: dayEl, |
5566
|
|
|
moreEl: moreEl, |
5567
|
|
|
segs: reslicedAllSegs, |
5568
|
|
|
hiddenSegs: reslicedHiddenSegs |
5569
|
|
|
}, ev); |
5570
|
|
|
} |
5571
|
|
|
|
5572
|
|
|
if (clickOption === 'popover') { |
5573
|
|
|
_this.showSegPopover(date, cell, moreEl, reslicedAllSegs); |
5574
|
|
|
} |
5575
|
|
|
else if (typeof clickOption === 'string') { // a view name |
5576
|
|
|
view.calendar.zoomTo(date, clickOption); |
5577
|
|
|
} |
5578
|
|
|
}); |
5579
|
|
|
}, |
5580
|
|
|
|
5581
|
|
|
|
5582
|
|
|
// Reveals the popover that displays all events within a cell |
5583
|
|
|
showSegPopover: function(date, cell, moreLink, segs) { |
5584
|
|
|
var _this = this; |
5585
|
|
|
var view = this.view; |
5586
|
|
|
var moreWrap = moreLink.parent(); // the <div> wrapper around the <a> |
5587
|
|
|
var topEl; // the element we want to match the top coordinate of |
5588
|
|
|
var options; |
5589
|
|
|
|
5590
|
|
|
if (view.rowCnt == 1) { |
5591
|
|
|
topEl = this.view.el; // will cause the popover to cover any sort of header |
5592
|
|
|
} |
5593
|
|
|
else { |
5594
|
|
|
topEl = this.rowEls.eq(cell.row); // will align with top of row |
5595
|
|
|
} |
5596
|
|
|
|
5597
|
|
|
options = { |
5598
|
|
|
className: 'fc-more-popover', |
5599
|
|
|
content: this.renderSegPopoverContent(date, segs), |
5600
|
|
|
parentEl: this.el, |
5601
|
|
|
top: topEl.offset().top, |
5602
|
|
|
autoHide: true, // when the user clicks elsewhere, hide the popover |
5603
|
|
|
viewportConstrain: view.opt('popoverViewportConstrain'), |
5604
|
|
|
hide: function() { |
5605
|
|
|
// destroy everything when the popover is hidden |
5606
|
|
|
_this.segPopover.destroy(); |
5607
|
|
|
_this.segPopover = null; |
5608
|
|
|
_this.popoverSegs = null; |
5609
|
|
|
} |
5610
|
|
|
}; |
5611
|
|
|
|
5612
|
|
|
// Determine horizontal coordinate. |
5613
|
|
|
// We use the moreWrap instead of the <td> to avoid border confusion. |
5614
|
|
|
if (view.opt('isRTL')) { |
5615
|
|
|
options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border |
5616
|
|
|
} |
5617
|
|
|
else { |
5618
|
|
|
options.left = moreWrap.offset().left - 1; // -1 to be over cell border |
5619
|
|
|
} |
5620
|
|
|
|
5621
|
|
|
this.segPopover = new Popover(options); |
5622
|
|
|
this.segPopover.show(); |
5623
|
|
|
}, |
5624
|
|
|
|
5625
|
|
|
|
5626
|
|
|
// Builds the inner DOM contents of the segment popover |
5627
|
|
|
renderSegPopoverContent: function(date, segs) { |
5628
|
|
|
var view = this.view; |
5629
|
|
|
var isTheme = view.opt('theme'); |
5630
|
|
|
var title = date.format(view.opt('dayPopoverFormat')); |
5631
|
|
|
var content = $( |
5632
|
|
|
'<div class="fc-header ' + view.widgetHeaderClass + '">' + |
5633
|
|
|
'<span class="fc-close ' + |
5634
|
|
|
(isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') + |
5635
|
|
|
'"></span>' + |
5636
|
|
|
'<span class="fc-title">' + |
5637
|
|
|
htmlEscape(title) + |
5638
|
|
|
'</span>' + |
5639
|
|
|
'<div class="fc-clear"/>' + |
5640
|
|
|
'</div>' + |
5641
|
|
|
'<div class="fc-body ' + view.widgetContentClass + '">' + |
5642
|
|
|
'<div class="fc-event-container"></div>' + |
5643
|
|
|
'</div>' |
5644
|
|
|
); |
5645
|
|
|
var segContainer = content.find('.fc-event-container'); |
5646
|
|
|
var i; |
5647
|
|
|
|
5648
|
|
|
// render each seg's `el` and only return the visible segs |
5649
|
|
|
segs = this.renderSegs(segs, true); // disableResizing=true |
5650
|
|
|
this.popoverSegs = segs; |
5651
|
|
|
|
5652
|
|
|
for (i = 0; i < segs.length; i++) { |
5653
|
|
|
|
5654
|
|
|
// because segments in the popover are not part of a grid coordinate system, provide a hint to any |
5655
|
|
|
// grids that want to do drag-n-drop about which cell it came from |
5656
|
|
|
segs[i].cellDate = date; |
5657
|
|
|
|
5658
|
|
|
segContainer.append(segs[i].el); |
5659
|
|
|
} |
5660
|
|
|
|
5661
|
|
|
return content; |
5662
|
|
|
}, |
5663
|
|
|
|
5664
|
|
|
|
5665
|
|
|
// Given the events within an array of segment objects, reslice them to be in a single day |
5666
|
|
|
resliceDaySegs: function(segs, dayDate) { |
5667
|
|
|
var events = $.map(segs, function(seg) { |
5668
|
|
|
return seg.event; |
5669
|
|
|
}); |
5670
|
|
|
var dayStart = dayDate.clone().stripTime(); |
5671
|
|
|
var dayEnd = dayStart.clone().add(1, 'days'); |
5672
|
|
|
|
5673
|
|
|
return this.eventsToSegs(events, dayStart, dayEnd); |
5674
|
|
|
}, |
5675
|
|
|
|
5676
|
|
|
|
5677
|
|
|
// Generates the text that should be inside a "more" link, given the number of events it represents |
5678
|
|
|
getMoreLinkText: function(num) { |
5679
|
|
|
var view = this.view; |
5680
|
|
|
var opt = view.opt('eventLimitText'); |
5681
|
|
|
|
5682
|
|
|
if (typeof opt === 'function') { |
5683
|
|
|
return opt(num); |
5684
|
|
|
} |
5685
|
|
|
else { |
5686
|
|
|
return '+' + num + ' ' + opt; |
5687
|
|
|
} |
5688
|
|
|
}, |
5689
|
|
|
|
5690
|
|
|
|
5691
|
|
|
// Returns segments within a given cell. |
5692
|
|
|
// If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. |
5693
|
|
|
getCellSegs: function(cell, startLevel) { |
5694
|
|
|
var segMatrix = this.rowStructs[cell.row].segMatrix; |
5695
|
|
|
var level = startLevel || 0; |
5696
|
|
|
var segs = []; |
5697
|
|
|
var seg; |
5698
|
|
|
|
5699
|
|
|
while (level < segMatrix.length) { |
5700
|
|
|
seg = segMatrix[level][cell.col]; |
5701
|
|
|
if (seg) { |
5702
|
|
|
segs.push(seg); |
5703
|
|
|
} |
5704
|
|
|
level++; |
5705
|
|
|
} |
5706
|
|
|
|
5707
|
|
|
return segs; |
5708
|
|
|
} |
5709
|
|
|
|
5710
|
|
|
}); |
5711
|
|
|
|
5712
|
|
|
;; |
5713
|
|
|
|
5714
|
|
|
/* A component that renders one or more columns of vertical time slots |
5715
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
5716
|
|
|
|
5717
|
|
|
function TimeGrid(view) { |
5718
|
|
|
Grid.call(this, view); // call the super-constructor |
5719
|
|
|
} |
5720
|
|
|
|
5721
|
|
|
|
5722
|
|
|
TimeGrid.prototype = createObject(Grid.prototype); // define the super-class |
5723
|
|
|
$.extend(TimeGrid.prototype, { |
5724
|
|
|
|
5725
|
|
|
slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines |
5726
|
|
|
snapDuration: null, // granularity of time for dragging and selecting |
5727
|
|
|
|
5728
|
|
|
minTime: null, // Duration object that denotes the first visible time of any given day |
5729
|
|
|
maxTime: null, // Duration object that denotes the exclusive visible end time of any given day |
5730
|
|
|
|
5731
|
|
|
dayEls: null, // cells elements in the day-row background |
5732
|
|
|
slatEls: null, // elements running horizontally across all columns |
5733
|
|
|
|
5734
|
|
|
slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot |
5735
|
|
|
|
5736
|
|
|
highlightEl: null, // cell skeleton element for rendering the highlight |
5737
|
|
|
helperEl: null, // cell skeleton element for rendering the mock event "helper" |
5738
|
|
|
|
5739
|
|
|
|
5740
|
|
|
// Renders the time grid into `this.el`, which should already be assigned. |
5741
|
|
|
// Relies on the view's colCnt. In the future, this component should probably be self-sufficient. |
5742
|
|
|
render: function() { |
5743
|
|
|
this.processOptions(); |
5744
|
|
|
|
5745
|
|
|
this.el.html(this.renderHtml()); |
5746
|
|
|
|
5747
|
|
|
this.dayEls = this.el.find('.fc-day'); |
5748
|
|
|
this.slatEls = this.el.find('.fc-slats tr'); |
5749
|
|
|
|
5750
|
|
|
this.computeSlatTops(); |
5751
|
|
|
|
5752
|
|
|
Grid.prototype.render.call(this); // call the super-method |
5753
|
|
|
}, |
5754
|
|
|
|
5755
|
|
|
|
5756
|
|
|
// Renders the basic HTML skeleton for the grid |
5757
|
|
|
renderHtml: function() { |
5758
|
|
|
return '' + |
5759
|
|
|
'<div class="fc-bg">' + |
5760
|
|
|
'<table>' + |
5761
|
|
|
this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml |
5762
|
|
|
'</table>' + |
5763
|
|
|
'</div>' + |
5764
|
|
|
'<div class="fc-slats">' + |
5765
|
|
|
'<table>' + |
5766
|
|
|
this.slatRowHtml() + |
5767
|
|
|
'</table>' + |
5768
|
|
|
'</div>'; |
5769
|
|
|
}, |
5770
|
|
|
|
5771
|
|
|
|
5772
|
|
|
// Renders the HTML for a vertical background cell behind the slots. |
5773
|
|
|
// This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering. |
5774
|
|
|
slotBgCellHtml: function(row, col, date) { |
5775
|
|
|
return this.bgCellHtml(row, col, date); |
5776
|
|
|
}, |
5777
|
|
|
|
5778
|
|
|
|
5779
|
|
|
// Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. |
5780
|
|
|
slatRowHtml: function() { |
5781
|
|
|
var view = this.view; |
5782
|
|
|
var calendar = view.calendar; |
5783
|
|
|
var isRTL = view.opt('isRTL'); |
5784
|
|
|
var html = ''; |
5785
|
|
|
var slotNormal = this.slotDuration.asMinutes() % 15 === 0; |
5786
|
|
|
var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations |
5787
|
|
|
var slotDate; // will be on the view's first day, but we only care about its time |
5788
|
|
|
var minutes; |
5789
|
|
|
var axisHtml; |
5790
|
|
|
|
5791
|
|
|
// Calculate the time for each slot |
5792
|
|
|
while (slotTime < this.maxTime) { |
5793
|
|
|
slotDate = view.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues |
5794
|
|
|
minutes = slotDate.minutes(); |
5795
|
|
|
|
5796
|
|
|
axisHtml = |
5797
|
|
|
'<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' + |
5798
|
|
|
((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time |
5799
|
|
|
'<span>' + // for matchCellWidths |
5800
|
|
|
htmlEscape(calendar.formatDate(slotDate, view.opt('axisFormat'))) + |
5801
|
|
|
'</span>' : |
5802
|
|
|
'' |
5803
|
|
|
) + |
5804
|
|
|
'</td>'; |
5805
|
|
|
|
5806
|
|
|
html += |
5807
|
|
|
'<tr ' + (!minutes ? '' : 'class="fc-minor"') + '>' + |
5808
|
|
|
(!isRTL ? axisHtml : '') + |
5809
|
|
|
'<td class="' + view.widgetContentClass + '"/>' + |
5810
|
|
|
(isRTL ? axisHtml : '') + |
5811
|
|
|
"</tr>"; |
5812
|
|
|
|
5813
|
|
|
slotTime.add(this.slotDuration); |
5814
|
|
|
} |
5815
|
|
|
|
5816
|
|
|
return html; |
5817
|
|
|
}, |
5818
|
|
|
|
5819
|
|
|
|
5820
|
|
|
// Parses various options into properties of this object |
5821
|
|
|
processOptions: function() { |
5822
|
|
|
var view = this.view; |
5823
|
|
|
var slotDuration = view.opt('slotDuration'); |
5824
|
|
|
var snapDuration = view.opt('snapDuration'); |
5825
|
|
|
|
5826
|
|
|
slotDuration = moment.duration(slotDuration); |
5827
|
|
|
snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; |
5828
|
|
|
|
5829
|
|
|
this.slotDuration = slotDuration; |
5830
|
|
|
this.snapDuration = snapDuration; |
5831
|
|
|
this.cellDuration = snapDuration; // important to assign this for Grid.events.js |
5832
|
|
|
|
5833
|
|
|
this.minTime = moment.duration(view.opt('minTime')); |
5834
|
|
|
this.maxTime = moment.duration(view.opt('maxTime')); |
5835
|
|
|
}, |
5836
|
|
|
|
5837
|
|
|
|
5838
|
|
|
// Slices up a date range into a segment for each column |
5839
|
|
|
rangeToSegs: function(rangeStart, rangeEnd) { |
5840
|
|
|
var view = this.view; |
5841
|
|
|
var segs = []; |
5842
|
|
|
var seg; |
5843
|
|
|
var col; |
5844
|
|
|
var cellDate; |
5845
|
|
|
var colStart, colEnd; |
5846
|
|
|
|
5847
|
|
|
// normalize |
5848
|
|
|
rangeStart = rangeStart.clone().stripZone(); |
5849
|
|
|
rangeEnd = rangeEnd.clone().stripZone(); |
5850
|
|
|
|
5851
|
|
|
for (col = 0; col < view.colCnt; col++) { |
5852
|
|
|
cellDate = view.cellToDate(0, col); // use the View's cell system for this |
5853
|
|
|
colStart = cellDate.clone().time(this.minTime); |
5854
|
|
|
colEnd = cellDate.clone().time(this.maxTime); |
5855
|
|
|
seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd); |
5856
|
|
|
if (seg) { |
5857
|
|
|
seg.col = col; |
5858
|
|
|
segs.push(seg); |
5859
|
|
|
} |
5860
|
|
|
} |
5861
|
|
|
|
5862
|
|
|
return segs; |
5863
|
|
|
}, |
5864
|
|
|
|
5865
|
|
|
|
5866
|
|
|
/* Coordinates |
5867
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
5868
|
|
|
|
5869
|
|
|
|
5870
|
|
|
// Called when there is a window resize/zoom and we need to recalculate coordinates for the grid |
5871
|
|
|
resize: function() { |
5872
|
|
|
this.computeSlatTops(); |
5873
|
|
|
this.updateSegVerticals(); |
5874
|
|
|
}, |
5875
|
|
|
|
5876
|
|
|
|
5877
|
|
|
// Populates the given empty `rows` and `cols` arrays with offset positions of the "snap" cells. |
5878
|
|
|
// "Snap" cells are different the slots because they might have finer granularity. |
5879
|
|
|
buildCoords: function(rows, cols) { |
5880
|
|
|
var colCnt = this.view.colCnt; |
5881
|
|
|
var originTop = this.el.offset().top; |
5882
|
|
|
var snapTime = moment.duration(+this.minTime); |
5883
|
|
|
var p = null; |
5884
|
|
|
var e, n; |
5885
|
|
|
|
5886
|
|
|
this.dayEls.slice(0, colCnt).each(function(i, _e) { |
5887
|
|
|
e = $(_e); |
5888
|
|
|
n = e.offset().left; |
5889
|
|
|
if (p) { |
5890
|
|
|
p[1] = n; |
5891
|
|
|
} |
5892
|
|
|
p = [ n ]; |
5893
|
|
|
cols[i] = p; |
5894
|
|
|
}); |
5895
|
|
|
p[1] = n + e.outerWidth(); |
5896
|
|
|
|
5897
|
|
|
p = null; |
5898
|
|
|
while (snapTime < this.maxTime) { |
5899
|
|
|
n = originTop + this.computeTimeTop(snapTime); |
5900
|
|
|
if (p) { |
5901
|
|
|
p[1] = n; |
5902
|
|
|
} |
5903
|
|
|
p = [ n ]; |
5904
|
|
|
rows.push(p); |
5905
|
|
|
snapTime.add(this.snapDuration); |
5906
|
|
|
} |
5907
|
|
|
p[1] = originTop + this.computeTimeTop(snapTime); // the position of the exclusive end |
5908
|
|
|
}, |
5909
|
|
|
|
5910
|
|
|
|
5911
|
|
|
// Gets the datetime for the given slot cell |
5912
|
|
|
getCellDate: function(cell) { |
5913
|
|
|
var view = this.view; |
5914
|
|
|
var calendar = view.calendar; |
5915
|
|
|
|
5916
|
|
|
return calendar.rezoneDate( // since we are adding a time, it needs to be in the calendar's timezone |
5917
|
|
|
view.cellToDate(0, cell.col) // View's coord system only accounts for start-of-day for column |
5918
|
|
|
.time(this.minTime + this.snapDuration * cell.row) |
5919
|
|
|
); |
5920
|
|
|
}, |
5921
|
|
|
|
5922
|
|
|
|
5923
|
|
|
// Gets the element that represents the whole-day the cell resides on |
5924
|
|
|
getCellDayEl: function(cell) { |
5925
|
|
|
return this.dayEls.eq(cell.col); |
5926
|
|
|
}, |
5927
|
|
|
|
5928
|
|
|
|
5929
|
|
|
// Computes the top coordinate, relative to the bounds of the grid, of the given date. |
5930
|
|
|
// A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. |
5931
|
|
|
computeDateTop: function(date, startOfDayDate) { |
5932
|
|
|
return this.computeTimeTop( |
5933
|
|
|
moment.duration( |
5934
|
|
|
date.clone().stripZone() - startOfDayDate.clone().stripTime() |
5935
|
|
|
) |
5936
|
|
|
); |
5937
|
|
|
}, |
5938
|
|
|
|
5939
|
|
|
|
5940
|
|
|
// Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). |
5941
|
|
|
computeTimeTop: function(time) { |
5942
|
|
|
var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered |
5943
|
|
|
var slatIndex; |
5944
|
|
|
var slatRemainder; |
5945
|
|
|
var slatTop; |
5946
|
|
|
var slatBottom; |
5947
|
|
|
|
5948
|
|
|
// constrain. because minTime/maxTime might be customized |
5949
|
|
|
slatCoverage = Math.max(0, slatCoverage); |
5950
|
|
|
slatCoverage = Math.min(this.slatEls.length, slatCoverage); |
5951
|
|
|
|
5952
|
|
|
slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot |
5953
|
|
|
slatRemainder = slatCoverage - slatIndex; |
5954
|
|
|
slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot |
5955
|
|
|
|
5956
|
|
|
if (slatRemainder) { // time spans part-way into the slot |
5957
|
|
|
slatBottom = this.slatTops[slatIndex + 1]; |
5958
|
|
|
return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots |
5959
|
|
|
} |
5960
|
|
|
else { |
5961
|
|
|
return slatTop; |
5962
|
|
|
} |
5963
|
|
|
}, |
5964
|
|
|
|
5965
|
|
|
|
5966
|
|
|
// Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`. |
5967
|
|
|
// Includes the the bottom of the last slat as the last item in the array. |
5968
|
|
|
computeSlatTops: function() { |
5969
|
|
|
var tops = []; |
5970
|
|
|
var top; |
5971
|
|
|
|
5972
|
|
|
this.slatEls.each(function(i, node) { |
5973
|
|
|
top = $(node).position().top; |
5974
|
|
|
tops.push(top); |
5975
|
|
|
}); |
5976
|
|
|
|
5977
|
|
|
tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat |
5978
|
|
|
|
5979
|
|
|
this.slatTops = tops; |
5980
|
|
|
}, |
5981
|
|
|
|
5982
|
|
|
|
5983
|
|
|
/* Event Drag Visualization |
5984
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
5985
|
|
|
|
5986
|
|
|
|
5987
|
|
|
// Renders a visual indication of an event being dragged over the specified date(s). |
5988
|
|
|
// `end` and `seg` can be null. See View's documentation on renderDrag for more info. |
5989
|
|
|
renderDrag: function(start, end, seg) { |
5990
|
|
|
var opacity; |
5991
|
|
|
|
5992
|
|
|
if (seg) { // if there is event information for this drag, render a helper event |
5993
|
|
|
this.renderRangeHelper(start, end, seg); |
5994
|
|
|
|
5995
|
|
|
opacity = this.view.opt('dragOpacity'); |
5996
|
|
|
if (opacity !== undefined) { |
5997
|
|
|
this.helperEl.css('opacity', opacity); |
5998
|
|
|
} |
5999
|
|
|
|
6000
|
|
|
return true; // signal that a helper has been rendered |
6001
|
|
|
} |
6002
|
|
|
else { |
6003
|
|
|
// otherwise, just render a highlight |
6004
|
|
|
this.renderHighlight( |
6005
|
|
|
start, |
6006
|
|
|
end || this.view.calendar.getDefaultEventEnd(false, start) |
6007
|
|
|
); |
|
|
|
|
6008
|
|
|
} |
6009
|
|
|
}, |
6010
|
|
|
|
6011
|
|
|
|
6012
|
|
|
// Unrenders any visual indication of an event being dragged |
6013
|
|
|
destroyDrag: function() { |
6014
|
|
|
this.destroyHelper(); |
6015
|
|
|
this.destroyHighlight(); |
6016
|
|
|
}, |
6017
|
|
|
|
6018
|
|
|
|
6019
|
|
|
/* Event Resize Visualization |
6020
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
6021
|
|
|
|
6022
|
|
|
|
6023
|
|
|
// Renders a visual indication of an event being resized |
6024
|
|
|
renderResize: function(start, end, seg) { |
6025
|
|
|
this.renderRangeHelper(start, end, seg); |
6026
|
|
|
}, |
6027
|
|
|
|
6028
|
|
|
|
6029
|
|
|
// Unrenders any visual indication of an event being resized |
6030
|
|
|
destroyResize: function() { |
6031
|
|
|
this.destroyHelper(); |
6032
|
|
|
}, |
6033
|
|
|
|
6034
|
|
|
|
6035
|
|
|
/* Event Helper |
6036
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
6037
|
|
|
|
6038
|
|
|
|
6039
|
|
|
// Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag) |
6040
|
|
|
renderHelper: function(event, sourceSeg) { |
6041
|
|
|
var res = this.renderEventTable([ event ]); |
6042
|
|
|
var tableEl = res.tableEl; |
6043
|
|
|
var segs = res.segs; |
6044
|
|
|
var i, seg; |
6045
|
|
|
var sourceEl; |
6046
|
|
|
|
6047
|
|
|
// Try to make the segment that is in the same row as sourceSeg look the same |
6048
|
|
|
for (i = 0; i < segs.length; i++) { |
6049
|
|
|
seg = segs[i]; |
6050
|
|
|
if (sourceSeg && sourceSeg.col === seg.col) { |
6051
|
|
|
sourceEl = sourceSeg.el; |
6052
|
|
|
seg.el.css({ |
6053
|
|
|
left: sourceEl.css('left'), |
6054
|
|
|
right: sourceEl.css('right'), |
6055
|
|
|
'margin-left': sourceEl.css('margin-left'), |
6056
|
|
|
'margin-right': sourceEl.css('margin-right') |
6057
|
|
|
}); |
6058
|
|
|
} |
6059
|
|
|
} |
6060
|
|
|
|
6061
|
|
|
this.helperEl = $('<div class="fc-helper-skeleton"/>') |
6062
|
|
|
.append(tableEl) |
6063
|
|
|
.appendTo(this.el); |
6064
|
|
|
}, |
6065
|
|
|
|
6066
|
|
|
|
6067
|
|
|
// Unrenders any mock helper event |
6068
|
|
|
destroyHelper: function() { |
6069
|
|
|
if (this.helperEl) { |
6070
|
|
|
this.helperEl.remove(); |
6071
|
|
|
this.helperEl = null; |
6072
|
|
|
} |
6073
|
|
|
}, |
6074
|
|
|
|
6075
|
|
|
|
6076
|
|
|
/* Selection |
6077
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
6078
|
|
|
|
6079
|
|
|
|
6080
|
|
|
// Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. |
6081
|
|
|
renderSelection: function(start, end) { |
6082
|
|
|
if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered |
6083
|
|
|
this.renderRangeHelper(start, end); |
6084
|
|
|
} |
6085
|
|
|
else { |
6086
|
|
|
this.renderHighlight(start, end); |
6087
|
|
|
} |
6088
|
|
|
}, |
6089
|
|
|
|
6090
|
|
|
|
6091
|
|
|
// Unrenders any visual indication of a selection |
6092
|
|
|
destroySelection: function() { |
6093
|
|
|
this.destroyHelper(); |
6094
|
|
|
this.destroyHighlight(); |
6095
|
|
|
}, |
6096
|
|
|
|
6097
|
|
|
|
6098
|
|
|
/* Highlight |
6099
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
6100
|
|
|
|
6101
|
|
|
|
6102
|
|
|
// Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive. |
6103
|
|
|
renderHighlight: function(start, end) { |
6104
|
|
|
this.highlightEl = $( |
6105
|
|
|
this.highlightSkeletonHtml(start, end) |
6106
|
|
|
).appendTo(this.el); |
6107
|
|
|
}, |
6108
|
|
|
|
6109
|
|
|
|
6110
|
|
|
// Unrenders the emphasis on a date range |
6111
|
|
|
destroyHighlight: function() { |
6112
|
|
|
if (this.highlightEl) { |
6113
|
|
|
this.highlightEl.remove(); |
6114
|
|
|
this.highlightEl = null; |
6115
|
|
|
} |
6116
|
|
|
}, |
6117
|
|
|
|
6118
|
|
|
|
6119
|
|
|
// Generates HTML for a table element with containers in each column, responsible for absolutely positioning the |
6120
|
|
|
// highlight elements to cover the highlighted slots. |
6121
|
|
|
highlightSkeletonHtml: function(start, end) { |
6122
|
|
|
var view = this.view; |
6123
|
|
|
var segs = this.rangeToSegs(start, end); |
6124
|
|
|
var cellHtml = ''; |
6125
|
|
|
var col = 0; |
6126
|
|
|
var i, seg; |
6127
|
|
|
var dayDate; |
6128
|
|
|
var top, bottom; |
6129
|
|
|
|
6130
|
|
|
for (i = 0; i < segs.length; i++) { // loop through the segments. one per column |
6131
|
|
|
seg = segs[i]; |
6132
|
|
|
|
6133
|
|
|
// need empty cells beforehand? |
6134
|
|
|
if (col < seg.col) { |
6135
|
|
|
cellHtml += '<td colspan="' + (seg.col - col) + '"/>'; |
6136
|
|
|
col = seg.col; |
6137
|
|
|
} |
6138
|
|
|
|
6139
|
|
|
// compute vertical position |
6140
|
|
|
dayDate = view.cellToDate(0, col); |
6141
|
|
|
top = this.computeDateTop(seg.start, dayDate); |
6142
|
|
|
bottom = this.computeDateTop(seg.end, dayDate); // the y position of the bottom edge |
6143
|
|
|
|
6144
|
|
|
// generate the cell HTML. bottom becomes negative because it needs to be a CSS value relative to the |
6145
|
|
|
// bottom edge of the zero-height container. |
6146
|
|
|
cellHtml += |
6147
|
|
|
'<td>' + |
6148
|
|
|
'<div class="fc-highlight-container">' + |
6149
|
|
|
'<div class="fc-highlight" style="top:' + top + 'px;bottom:-' + bottom + 'px"/>' + |
6150
|
|
|
'</div>' + |
6151
|
|
|
'</td>'; |
6152
|
|
|
|
6153
|
|
|
col++; |
6154
|
|
|
} |
6155
|
|
|
|
6156
|
|
|
// need empty cells after the last segment? |
6157
|
|
|
if (col < view.colCnt) { |
6158
|
|
|
cellHtml += '<td colspan="' + (view.colCnt - col) + '"/>'; |
6159
|
|
|
} |
6160
|
|
|
|
6161
|
|
|
cellHtml = this.bookendCells(cellHtml, 'highlight'); |
6162
|
|
|
|
6163
|
|
|
return '' + |
6164
|
|
|
'<div class="fc-highlight-skeleton">' + |
6165
|
|
|
'<table>' + |
6166
|
|
|
'<tr>' + |
6167
|
|
|
cellHtml + |
6168
|
|
|
'</tr>' + |
6169
|
|
|
'</table>' + |
6170
|
|
|
'</div>'; |
6171
|
|
|
} |
6172
|
|
|
|
6173
|
|
|
}); |
6174
|
|
|
|
6175
|
|
|
;; |
6176
|
|
|
|
6177
|
|
|
/* Event-rendering methods for the TimeGrid class |
6178
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
6179
|
|
|
|
6180
|
|
|
$.extend(TimeGrid.prototype, { |
6181
|
|
|
|
6182
|
|
|
segs: null, // segment objects rendered in the component. null of events haven't been rendered yet |
6183
|
|
|
eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements |
6184
|
|
|
|
6185
|
|
|
|
6186
|
|
|
// Renders the events onto the grid and returns an array of segments that have been rendered |
6187
|
|
|
renderEvents: function(events) { |
6188
|
|
|
var res = this.renderEventTable(events); |
6189
|
|
|
|
6190
|
|
|
this.eventSkeletonEl = $('<div class="fc-content-skeleton"/>').append(res.tableEl); |
6191
|
|
|
this.el.append(this.eventSkeletonEl); |
6192
|
|
|
|
6193
|
|
|
this.segs = res.segs; |
6194
|
|
|
}, |
6195
|
|
|
|
6196
|
|
|
|
6197
|
|
|
// Retrieves rendered segment objects |
6198
|
|
|
getSegs: function() { |
6199
|
|
|
return this.segs || []; |
6200
|
|
|
}, |
6201
|
|
|
|
6202
|
|
|
|
6203
|
|
|
// Removes all event segment elements from the view |
6204
|
|
|
destroyEvents: function() { |
6205
|
|
|
Grid.prototype.destroyEvents.call(this); // call the super-method |
6206
|
|
|
|
6207
|
|
|
if (this.eventSkeletonEl) { |
6208
|
|
|
this.eventSkeletonEl.remove(); |
6209
|
|
|
this.eventSkeletonEl = null; |
6210
|
|
|
} |
6211
|
|
|
|
6212
|
|
|
this.segs = null; |
6213
|
|
|
}, |
6214
|
|
|
|
6215
|
|
|
|
6216
|
|
|
// Renders and returns the <table> portion of the event-skeleton. |
6217
|
|
|
// Returns an object with properties 'tbodyEl' and 'segs'. |
6218
|
|
|
renderEventTable: function(events) { |
6219
|
|
|
var tableEl = $('<table><tr/></table>'); |
6220
|
|
|
var trEl = tableEl.find('tr'); |
6221
|
|
|
var segs = this.eventsToSegs(events); |
6222
|
|
|
var segCols; |
6223
|
|
|
var i, seg; |
6224
|
|
|
var col, colSegs; |
6225
|
|
|
var containerEl; |
6226
|
|
|
|
6227
|
|
|
segs = this.renderSegs(segs); // returns only the visible segs |
6228
|
|
|
segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg |
6229
|
|
|
|
6230
|
|
|
this.computeSegVerticals(segs); // compute and assign top/bottom |
6231
|
|
|
|
6232
|
|
|
for (col = 0; col < segCols.length; col++) { // iterate each column grouping |
6233
|
|
|
colSegs = segCols[col]; |
6234
|
|
|
placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array |
6235
|
|
|
|
6236
|
|
|
containerEl = $('<div class="fc-event-container"/>'); |
6237
|
|
|
|
6238
|
|
|
// assign positioning CSS and insert into container |
6239
|
|
|
for (i = 0; i < colSegs.length; i++) { |
6240
|
|
|
seg = colSegs[i]; |
6241
|
|
|
seg.el.css(this.generateSegPositionCss(seg)); |
6242
|
|
|
|
6243
|
|
|
// if the height is short, add a className for alternate styling |
6244
|
|
|
if (seg.bottom - seg.top < 30) { |
6245
|
|
|
seg.el.addClass('fc-short'); |
6246
|
|
|
} |
6247
|
|
|
|
6248
|
|
|
containerEl.append(seg.el); |
6249
|
|
|
} |
6250
|
|
|
|
6251
|
|
|
trEl.append($('<td/>').append(containerEl)); |
6252
|
|
|
} |
6253
|
|
|
|
6254
|
|
|
this.bookendCells(trEl, 'eventSkeleton'); |
6255
|
|
|
|
6256
|
|
|
return { |
6257
|
|
|
tableEl: tableEl, |
6258
|
|
|
segs: segs |
6259
|
|
|
}; |
6260
|
|
|
}, |
6261
|
|
|
|
6262
|
|
|
|
6263
|
|
|
// Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom. |
6264
|
|
|
updateSegVerticals: function() { |
6265
|
|
|
var segs = this.segs; |
6266
|
|
|
var i; |
6267
|
|
|
|
6268
|
|
|
if (segs) { |
6269
|
|
|
this.computeSegVerticals(segs); |
6270
|
|
|
|
6271
|
|
|
for (i = 0; i < segs.length; i++) { |
6272
|
|
|
segs[i].el.css( |
6273
|
|
|
this.generateSegVerticalCss(segs[i]) |
6274
|
|
|
); |
6275
|
|
|
} |
6276
|
|
|
} |
6277
|
|
|
}, |
6278
|
|
|
|
6279
|
|
|
|
6280
|
|
|
// For each segment in an array, computes and assigns its top and bottom properties |
6281
|
|
|
computeSegVerticals: function(segs) { |
6282
|
|
|
var i, seg; |
6283
|
|
|
|
6284
|
|
|
for (i = 0; i < segs.length; i++) { |
6285
|
|
|
seg = segs[i]; |
6286
|
|
|
seg.top = this.computeDateTop(seg.start, seg.start); |
6287
|
|
|
seg.bottom = this.computeDateTop(seg.end, seg.start); |
6288
|
|
|
} |
6289
|
|
|
}, |
6290
|
|
|
|
6291
|
|
|
|
6292
|
|
|
// Renders the HTML for a single event segment's default rendering |
6293
|
|
|
renderSegHtml: function(seg, disableResizing) { |
6294
|
|
|
var view = this.view; |
6295
|
|
|
var event = seg.event; |
6296
|
|
|
var isDraggable = view.isEventDraggable(event); |
6297
|
|
|
var isResizable = !disableResizing && seg.isEnd && view.isEventResizable(event); |
6298
|
|
|
var classes = this.getSegClasses(seg, isDraggable, isResizable); |
6299
|
|
|
var skinCss = this.getEventSkinCss(event); |
6300
|
|
|
var timeText; |
6301
|
|
|
var fullTimeText; // more verbose time text. for the print stylesheet |
6302
|
|
|
var startTimeText; // just the start time text |
6303
|
|
|
|
6304
|
|
|
classes.unshift('fc-time-grid-event'); |
6305
|
|
|
|
6306
|
|
|
if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day... |
6307
|
|
|
// Don't display time text on segments that run entirely through a day. |
6308
|
|
|
// That would appear as midnight-midnight and would look dumb. |
6309
|
|
|
// Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am) |
6310
|
|
|
if (seg.isStart || seg.isEnd) { |
6311
|
|
|
timeText = view.getEventTimeText(seg.start, seg.end); |
6312
|
|
|
fullTimeText = view.getEventTimeText(seg.start, seg.end, 'LT'); |
6313
|
|
|
startTimeText = view.getEventTimeText(seg.start, null); |
6314
|
|
|
} |
6315
|
|
|
} else { |
6316
|
|
|
// Display the normal time text for the *event's* times |
6317
|
|
|
timeText = view.getEventTimeText(event); |
6318
|
|
|
fullTimeText = view.getEventTimeText(event, 'LT'); |
6319
|
|
|
startTimeText = view.getEventTimeText(event.start, null); |
6320
|
|
|
} |
6321
|
|
|
|
6322
|
|
|
return '<a class="' + classes.join(' ') + '"' + |
6323
|
|
|
(event.url ? |
6324
|
|
|
' href="' + htmlEscape(event.url) + '"' : |
6325
|
|
|
'' |
6326
|
|
|
) + |
6327
|
|
|
(skinCss ? |
6328
|
|
|
' style="' + skinCss + '"' : |
6329
|
|
|
'' |
6330
|
|
|
) + |
6331
|
|
|
'>' + |
6332
|
|
|
'<div class="fc-content">' + |
6333
|
|
|
(timeText ? |
6334
|
|
|
'<div class="fc-time"' + |
6335
|
|
|
' data-start="' + htmlEscape(startTimeText) + '"' + |
|
|
|
|
6336
|
|
|
' data-full="' + htmlEscape(fullTimeText) + '"' + |
|
|
|
|
6337
|
|
|
'>' + |
6338
|
|
|
'<span>' + htmlEscape(timeText) + '</span>' + |
6339
|
|
|
'</div>' : |
6340
|
|
|
'' |
6341
|
|
|
) + |
6342
|
|
|
(event.title ? |
6343
|
|
|
'<div class="fc-title">' + |
6344
|
|
|
htmlEscape(event.title) + |
6345
|
|
|
'</div>' : |
6346
|
|
|
'' |
6347
|
|
|
) + |
6348
|
|
|
'</div>' + |
6349
|
|
|
'<div class="fc-bg"/>' + |
6350
|
|
|
(isResizable ? |
6351
|
|
|
'<div class="fc-resizer"/>' : |
6352
|
|
|
'' |
6353
|
|
|
) + |
6354
|
|
|
'</a>'; |
6355
|
|
|
}, |
6356
|
|
|
|
6357
|
|
|
|
6358
|
|
|
// Generates an object with CSS properties/values that should be applied to an event segment element. |
6359
|
|
|
// Contains important positioning-related properties that should be applied to any event element, customized or not. |
6360
|
|
|
generateSegPositionCss: function(seg) { |
6361
|
|
|
var view = this.view; |
6362
|
|
|
var isRTL = view.opt('isRTL'); |
6363
|
|
|
var shouldOverlap = view.opt('slotEventOverlap'); |
6364
|
|
|
var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point |
6365
|
|
|
var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point |
6366
|
|
|
var props = this.generateSegVerticalCss(seg); // get top/bottom first |
6367
|
|
|
var left; // amount of space from left edge, a fraction of the total width |
6368
|
|
|
var right; // amount of space from right edge, a fraction of the total width |
6369
|
|
|
|
6370
|
|
|
if (shouldOverlap) { |
6371
|
|
|
// double the width, but don't go beyond the maximum forward coordinate (1.0) |
6372
|
|
|
forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2); |
6373
|
|
|
} |
6374
|
|
|
|
6375
|
|
|
if (isRTL) { |
6376
|
|
|
left = 1 - forwardCoord; |
6377
|
|
|
right = backwardCoord; |
6378
|
|
|
} |
6379
|
|
|
else { |
6380
|
|
|
left = backwardCoord; |
6381
|
|
|
right = 1 - forwardCoord; |
6382
|
|
|
} |
6383
|
|
|
|
6384
|
|
|
props.zIndex = seg.level + 1; // convert from 0-base to 1-based |
6385
|
|
|
props.left = left * 100 + '%'; |
6386
|
|
|
props.right = right * 100 + '%'; |
6387
|
|
|
|
6388
|
|
|
if (shouldOverlap && seg.forwardPressure) { |
6389
|
|
|
// add padding to the edge so that forward stacked events don't cover the resizer's icon |
6390
|
|
|
props[isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width |
6391
|
|
|
} |
6392
|
|
|
|
6393
|
|
|
return props; |
6394
|
|
|
}, |
6395
|
|
|
|
6396
|
|
|
|
6397
|
|
|
// Generates an object with CSS properties for the top/bottom coordinates of a segment element |
6398
|
|
|
generateSegVerticalCss: function(seg) { |
6399
|
|
|
return { |
6400
|
|
|
top: seg.top, |
6401
|
|
|
bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container |
6402
|
|
|
}; |
6403
|
|
|
}, |
6404
|
|
|
|
6405
|
|
|
|
6406
|
|
|
// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col |
6407
|
|
|
groupSegCols: function(segs) { |
6408
|
|
|
var view = this.view; |
6409
|
|
|
var segCols = []; |
6410
|
|
|
var i; |
6411
|
|
|
|
6412
|
|
|
for (i = 0; i < view.colCnt; i++) { |
6413
|
|
|
segCols.push([]); |
6414
|
|
|
} |
6415
|
|
|
|
6416
|
|
|
for (i = 0; i < segs.length; i++) { |
6417
|
|
|
segCols[segs[i].col].push(segs[i]); |
6418
|
|
|
} |
6419
|
|
|
|
6420
|
|
|
return segCols; |
6421
|
|
|
} |
6422
|
|
|
|
6423
|
|
|
}); |
6424
|
|
|
|
6425
|
|
|
|
6426
|
|
|
// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. |
6427
|
|
|
// Also reorders the given array by date! |
6428
|
|
|
function placeSlotSegs(segs) { |
6429
|
|
|
var levels; |
6430
|
|
|
var level0; |
6431
|
|
|
var i; |
6432
|
|
|
|
6433
|
|
|
segs.sort(compareSegs); // order by date |
6434
|
|
|
levels = buildSlotSegLevels(segs); |
6435
|
|
|
computeForwardSlotSegs(levels); |
6436
|
|
|
|
6437
|
|
|
if ((level0 = levels[0])) { |
6438
|
|
|
|
6439
|
|
|
for (i = 0; i < level0.length; i++) { |
6440
|
|
|
computeSlotSegPressures(level0[i]); |
6441
|
|
|
} |
6442
|
|
|
|
6443
|
|
|
for (i = 0; i < level0.length; i++) { |
6444
|
|
|
computeSlotSegCoords(level0[i], 0, 0); |
6445
|
|
|
} |
6446
|
|
|
} |
6447
|
|
|
} |
6448
|
|
|
|
6449
|
|
|
|
6450
|
|
|
// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is |
6451
|
|
|
// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date. |
6452
|
|
|
function buildSlotSegLevels(segs) { |
6453
|
|
|
var levels = []; |
6454
|
|
|
var i, seg; |
6455
|
|
|
var j; |
6456
|
|
|
|
6457
|
|
|
for (i=0; i<segs.length; i++) { |
6458
|
|
|
seg = segs[i]; |
6459
|
|
|
|
6460
|
|
|
// go through all the levels and stop on the first level where there are no collisions |
6461
|
|
|
for (j=0; j<levels.length; j++) { |
6462
|
|
|
if (!computeSlotSegCollisions(seg, levels[j]).length) { |
6463
|
|
|
break; |
6464
|
|
|
} |
6465
|
|
|
} |
6466
|
|
|
|
6467
|
|
|
seg.level = j; |
6468
|
|
|
|
6469
|
|
|
(levels[j] || (levels[j] = [])).push(seg); |
6470
|
|
|
} |
6471
|
|
|
|
6472
|
|
|
return levels; |
6473
|
|
|
} |
6474
|
|
|
|
6475
|
|
|
|
6476
|
|
|
// For every segment, figure out the other segments that are in subsequent |
6477
|
|
|
// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs |
6478
|
|
|
function computeForwardSlotSegs(levels) { |
6479
|
|
|
var i, level; |
6480
|
|
|
var j, seg; |
6481
|
|
|
var k; |
6482
|
|
|
|
6483
|
|
|
for (i=0; i<levels.length; i++) { |
6484
|
|
|
level = levels[i]; |
6485
|
|
|
|
6486
|
|
|
for (j=0; j<level.length; j++) { |
6487
|
|
|
seg = level[j]; |
6488
|
|
|
|
6489
|
|
|
seg.forwardSegs = []; |
6490
|
|
|
for (k=i+1; k<levels.length; k++) { |
6491
|
|
|
computeSlotSegCollisions(seg, levels[k], seg.forwardSegs); |
6492
|
|
|
} |
6493
|
|
|
} |
6494
|
|
|
} |
6495
|
|
|
} |
6496
|
|
|
|
6497
|
|
|
|
6498
|
|
|
// Figure out which path forward (via seg.forwardSegs) results in the longest path until |
6499
|
|
|
// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure |
6500
|
|
|
function computeSlotSegPressures(seg) { |
6501
|
|
|
var forwardSegs = seg.forwardSegs; |
6502
|
|
|
var forwardPressure = 0; |
6503
|
|
|
var i, forwardSeg; |
6504
|
|
|
|
6505
|
|
|
if (seg.forwardPressure === undefined) { // not already computed |
6506
|
|
|
|
6507
|
|
|
for (i=0; i<forwardSegs.length; i++) { |
6508
|
|
|
forwardSeg = forwardSegs[i]; |
6509
|
|
|
|
6510
|
|
|
// figure out the child's maximum forward path |
6511
|
|
|
computeSlotSegPressures(forwardSeg); |
6512
|
|
|
|
6513
|
|
|
// either use the existing maximum, or use the child's forward pressure |
6514
|
|
|
// plus one (for the forwardSeg itself) |
6515
|
|
|
forwardPressure = Math.max( |
6516
|
|
|
forwardPressure, |
6517
|
|
|
1 + forwardSeg.forwardPressure |
6518
|
|
|
); |
6519
|
|
|
} |
6520
|
|
|
|
6521
|
|
|
seg.forwardPressure = forwardPressure; |
6522
|
|
|
} |
6523
|
|
|
} |
6524
|
|
|
|
6525
|
|
|
|
6526
|
|
|
// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range |
6527
|
|
|
// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and |
6528
|
|
|
// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left. |
6529
|
|
|
// |
6530
|
|
|
// The segment might be part of a "series", which means consecutive segments with the same pressure |
6531
|
|
|
// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of |
6532
|
|
|
// segments behind this one in the current series, and `seriesBackwardCoord` is the starting |
6533
|
|
|
// coordinate of the first segment in the series. |
6534
|
|
|
function computeSlotSegCoords(seg, seriesBackwardPressure, seriesBackwardCoord) { |
6535
|
|
|
var forwardSegs = seg.forwardSegs; |
6536
|
|
|
var i; |
6537
|
|
|
|
6538
|
|
|
if (seg.forwardCoord === undefined) { // not already computed |
6539
|
|
|
|
6540
|
|
|
if (!forwardSegs.length) { |
6541
|
|
|
|
6542
|
|
|
// if there are no forward segments, this segment should butt up against the edge |
6543
|
|
|
seg.forwardCoord = 1; |
6544
|
|
|
} |
6545
|
|
|
else { |
6546
|
|
|
|
6547
|
|
|
// sort highest pressure first |
6548
|
|
|
forwardSegs.sort(compareForwardSlotSegs); |
6549
|
|
|
|
6550
|
|
|
// this segment's forwardCoord will be calculated from the backwardCoord of the |
6551
|
|
|
// highest-pressure forward segment. |
6552
|
|
|
computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord); |
6553
|
|
|
seg.forwardCoord = forwardSegs[0].backwardCoord; |
6554
|
|
|
} |
6555
|
|
|
|
6556
|
|
|
// calculate the backwardCoord from the forwardCoord. consider the series |
6557
|
|
|
seg.backwardCoord = seg.forwardCoord - |
6558
|
|
|
(seg.forwardCoord - seriesBackwardCoord) / // available width for series |
6559
|
|
|
(seriesBackwardPressure + 1); // # of segments in the series |
6560
|
|
|
|
6561
|
|
|
// use this segment's coordinates to computed the coordinates of the less-pressurized |
6562
|
|
|
// forward segments |
6563
|
|
|
for (i=0; i<forwardSegs.length; i++) { |
6564
|
|
|
computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord); |
6565
|
|
|
} |
6566
|
|
|
} |
6567
|
|
|
} |
6568
|
|
|
|
6569
|
|
|
|
6570
|
|
|
// Find all the segments in `otherSegs` that vertically collide with `seg`. |
6571
|
|
|
// Append into an optionally-supplied `results` array and return. |
6572
|
|
|
function computeSlotSegCollisions(seg, otherSegs, results) { |
6573
|
|
|
results = results || []; |
6574
|
|
|
|
6575
|
|
|
for (var i=0; i<otherSegs.length; i++) { |
6576
|
|
|
if (isSlotSegCollision(seg, otherSegs[i])) { |
6577
|
|
|
results.push(otherSegs[i]); |
6578
|
|
|
} |
6579
|
|
|
} |
6580
|
|
|
|
6581
|
|
|
return results; |
6582
|
|
|
} |
6583
|
|
|
|
6584
|
|
|
|
6585
|
|
|
// Do these segments occupy the same vertical space? |
6586
|
|
|
function isSlotSegCollision(seg1, seg2) { |
6587
|
|
|
return seg1.bottom > seg2.top && seg1.top < seg2.bottom; |
6588
|
|
|
} |
6589
|
|
|
|
6590
|
|
|
|
6591
|
|
|
// A cmp function for determining which forward segment to rely on more when computing coordinates. |
6592
|
|
|
function compareForwardSlotSegs(seg1, seg2) { |
6593
|
|
|
// put higher-pressure first |
6594
|
|
|
return seg2.forwardPressure - seg1.forwardPressure || |
6595
|
|
|
// put segments that are closer to initial edge first (and favor ones with no coords yet) |
6596
|
|
|
(seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || |
6597
|
|
|
// do normal sorting... |
6598
|
|
|
compareSegs(seg1, seg2); |
6599
|
|
|
} |
6600
|
|
|
|
6601
|
|
|
;; |
6602
|
|
|
|
6603
|
|
|
/* An abstract class from which other views inherit from |
6604
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
6605
|
|
|
// Newer methods should be written as prototype methods, not in the monster `View` function at the bottom. |
6606
|
|
|
|
6607
|
|
|
View.prototype = { |
6608
|
|
|
|
6609
|
|
|
calendar: null, // owner Calendar object |
6610
|
|
|
coordMap: null, // a CoordMap object for converting pixel regions to dates |
6611
|
|
|
el: null, // the view's containing element. set by Calendar |
6612
|
|
|
|
6613
|
|
|
// important Moments |
6614
|
|
|
start: null, // the date of the very first cell |
6615
|
|
|
end: null, // the date after the very last cell |
6616
|
|
|
intervalStart: null, // the start of the interval of time the view represents (1st of month for month view) |
6617
|
|
|
intervalEnd: null, // the exclusive end of the interval of time the view represents |
6618
|
|
|
|
6619
|
|
|
// used for cell-to-date and date-to-cell calculations |
6620
|
|
|
rowCnt: null, // # of weeks |
6621
|
|
|
colCnt: null, // # of days displayed in a week |
6622
|
|
|
|
6623
|
|
|
isSelected: false, // boolean whether cells are user-selected or not |
6624
|
|
|
|
6625
|
|
|
// subclasses can optionally use a scroll container |
6626
|
|
|
scrollerEl: null, // the element that will most likely scroll when content is too tall |
6627
|
|
|
scrollTop: null, // cached vertical scroll value |
6628
|
|
|
|
6629
|
|
|
// classNames styled by jqui themes |
6630
|
|
|
widgetHeaderClass: null, |
6631
|
|
|
widgetContentClass: null, |
6632
|
|
|
highlightStateClass: null, |
6633
|
|
|
|
6634
|
|
|
// document handlers, bound to `this` object |
6635
|
|
|
documentMousedownProxy: null, |
6636
|
|
|
documentDragStartProxy: null, |
6637
|
|
|
|
6638
|
|
|
|
6639
|
|
|
// Serves as a "constructor" to suppliment the monster `View` constructor below |
6640
|
|
|
init: function() { |
6641
|
|
|
var tm = this.opt('theme') ? 'ui' : 'fc'; |
6642
|
|
|
|
6643
|
|
|
this.widgetHeaderClass = tm + '-widget-header'; |
6644
|
|
|
this.widgetContentClass = tm + '-widget-content'; |
6645
|
|
|
this.highlightStateClass = tm + '-state-highlight'; |
6646
|
|
|
|
6647
|
|
|
// save references to `this`-bound handlers |
6648
|
|
|
this.documentMousedownProxy = $.proxy(this, 'documentMousedown'); |
6649
|
|
|
this.documentDragStartProxy = $.proxy(this, 'documentDragStart'); |
6650
|
|
|
}, |
6651
|
|
|
|
6652
|
|
|
|
6653
|
|
|
// Renders the view inside an already-defined `this.el`. |
6654
|
|
|
// Subclasses should override this and then call the super method afterwards. |
6655
|
|
|
render: function() { |
6656
|
|
|
this.updateSize(); |
6657
|
|
|
this.trigger('viewRender', this, this, this.el); |
6658
|
|
|
|
6659
|
|
|
// attach handlers to document. do it here to allow for destroy/rerender |
6660
|
|
|
$(document) |
6661
|
|
|
.on('mousedown', this.documentMousedownProxy) |
6662
|
|
|
.on('dragstart', this.documentDragStartProxy); // jqui drag |
6663
|
|
|
}, |
6664
|
|
|
|
6665
|
|
|
|
6666
|
|
|
// Clears all view rendering, event elements, and unregisters handlers |
6667
|
|
|
destroy: function() { |
6668
|
|
|
this.unselect(); |
6669
|
|
|
this.trigger('viewDestroy', this, this, this.el); |
6670
|
|
|
this.destroyEvents(); |
6671
|
|
|
this.el.empty(); // removes inner contents but leaves the element intact |
6672
|
|
|
|
6673
|
|
|
$(document) |
6674
|
|
|
.off('mousedown', this.documentMousedownProxy) |
6675
|
|
|
.off('dragstart', this.documentDragStartProxy); |
6676
|
|
|
}, |
6677
|
|
|
|
6678
|
|
|
|
6679
|
|
|
// Used to determine what happens when the users clicks next/prev. Given -1 for prev, 1 for next. |
6680
|
|
|
// Should apply the delta to `date` (a Moment) and return it. |
6681
|
|
|
incrementDate: function(date, delta) { |
6682
|
|
|
// subclasses should implement |
6683
|
|
|
}, |
6684
|
|
|
|
6685
|
|
|
|
6686
|
|
|
/* Dimensions |
6687
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
6688
|
|
|
|
6689
|
|
|
|
6690
|
|
|
// Refreshes anything dependant upon sizing of the container element of the grid |
6691
|
|
|
updateSize: function(isResize) { |
6692
|
|
|
if (isResize) { |
6693
|
|
|
this.recordScroll(); |
6694
|
|
|
} |
6695
|
|
|
this.updateHeight(); |
6696
|
|
|
this.updateWidth(); |
6697
|
|
|
}, |
6698
|
|
|
|
6699
|
|
|
|
6700
|
|
|
// Refreshes the horizontal dimensions of the calendar |
6701
|
|
|
updateWidth: function() { |
6702
|
|
|
// subclasses should implement |
6703
|
|
|
}, |
6704
|
|
|
|
6705
|
|
|
|
6706
|
|
|
// Refreshes the vertical dimensions of the calendar |
6707
|
|
|
updateHeight: function() { |
6708
|
|
|
var calendar = this.calendar; // we poll the calendar for height information |
6709
|
|
|
|
6710
|
|
|
this.setHeight( |
6711
|
|
|
calendar.getSuggestedViewHeight(), |
6712
|
|
|
calendar.isHeightAuto() |
6713
|
|
|
); |
6714
|
|
|
}, |
6715
|
|
|
|
6716
|
|
|
|
6717
|
|
|
// Updates the vertical dimensions of the calendar to the specified height. |
6718
|
|
|
// if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height. |
6719
|
|
|
setHeight: function(height, isAuto) { |
6720
|
|
|
// subclasses should implement |
6721
|
|
|
}, |
6722
|
|
|
|
6723
|
|
|
|
6724
|
|
|
// Given the total height of the view, return the number of pixels that should be used for the scroller. |
6725
|
|
|
// Utility for subclasses. |
6726
|
|
|
computeScrollerHeight: function(totalHeight) { |
6727
|
|
|
var both = this.el.add(this.scrollerEl); |
6728
|
|
|
var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders) |
6729
|
|
|
|
6730
|
|
|
// fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked |
6731
|
|
|
both.css({ |
6732
|
|
|
position: 'relative', // cause a reflow, which will force fresh dimension recalculation |
6733
|
|
|
left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll |
6734
|
|
|
}); |
6735
|
|
|
otherHeight = this.el.outerHeight() - this.scrollerEl.height(); // grab the dimensions |
6736
|
|
|
both.css({ position: '', left: '' }); // undo hack |
6737
|
|
|
|
6738
|
|
|
return totalHeight - otherHeight; |
6739
|
|
|
}, |
6740
|
|
|
|
6741
|
|
|
|
6742
|
|
|
// Called for remembering the current scroll value of the scroller. |
6743
|
|
|
// Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently |
6744
|
|
|
// change the scroll of the container. |
6745
|
|
|
recordScroll: function() { |
6746
|
|
|
if (this.scrollerEl) { |
6747
|
|
|
this.scrollTop = this.scrollerEl.scrollTop(); |
6748
|
|
|
} |
6749
|
|
|
}, |
6750
|
|
|
|
6751
|
|
|
|
6752
|
|
|
// Set the scroll value of the scroller to the previously recorded value. |
6753
|
|
|
// Should be called after we know the view's dimensions have been restored following some type of destructive |
6754
|
|
|
// operation (like temporarily removing DOM elements). |
6755
|
|
|
restoreScroll: function() { |
6756
|
|
|
if (this.scrollTop !== null) { |
6757
|
|
|
this.scrollerEl.scrollTop(this.scrollTop); |
6758
|
|
|
} |
6759
|
|
|
}, |
6760
|
|
|
|
6761
|
|
|
|
6762
|
|
|
/* Events |
6763
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
6764
|
|
|
|
6765
|
|
|
|
6766
|
|
|
// Renders the events onto the view. |
6767
|
|
|
// Should be overriden by subclasses. Subclasses should call the super-method afterwards. |
6768
|
|
|
renderEvents: function(events) { |
6769
|
|
|
this.segEach(function(seg) { |
6770
|
|
|
this.trigger('eventAfterRender', seg.event, seg.event, seg.el); |
6771
|
|
|
}); |
6772
|
|
|
this.trigger('eventAfterAllRender'); |
6773
|
|
|
}, |
6774
|
|
|
|
6775
|
|
|
|
6776
|
|
|
// Removes event elements from the view. |
6777
|
|
|
// Should be overridden by subclasses. Should call this super-method FIRST, then subclass DOM destruction. |
6778
|
|
|
destroyEvents: function() { |
6779
|
|
|
this.segEach(function(seg) { |
6780
|
|
|
this.trigger('eventDestroy', seg.event, seg.event, seg.el); |
6781
|
|
|
}); |
6782
|
|
|
}, |
6783
|
|
|
|
6784
|
|
|
|
6785
|
|
|
// Given an event and the default element used for rendering, returns the element that should actually be used. |
6786
|
|
|
// Basically runs events and elements through the eventRender hook. |
6787
|
|
|
resolveEventEl: function(event, el) { |
6788
|
|
|
var custom = this.trigger('eventRender', event, event, el); |
6789
|
|
|
|
6790
|
|
|
if (custom === false) { // means don't render at all |
6791
|
|
|
el = null; |
6792
|
|
|
} |
6793
|
|
|
else if (custom && custom !== true) { |
6794
|
|
|
el = $(custom); |
6795
|
|
|
} |
6796
|
|
|
|
6797
|
|
|
return el; |
6798
|
|
|
}, |
6799
|
|
|
|
6800
|
|
|
|
6801
|
|
|
// Hides all rendered event segments linked to the given event |
6802
|
|
|
showEvent: function(event) { |
6803
|
|
|
this.segEach(function(seg) { |
6804
|
|
|
seg.el.css('visibility', ''); |
6805
|
|
|
}, event); |
6806
|
|
|
}, |
6807
|
|
|
|
6808
|
|
|
|
6809
|
|
|
// Shows all rendered event segments linked to the given event |
6810
|
|
|
hideEvent: function(event) { |
6811
|
|
|
this.segEach(function(seg) { |
6812
|
|
|
seg.el.css('visibility', 'hidden'); |
6813
|
|
|
}, event); |
6814
|
|
|
}, |
6815
|
|
|
|
6816
|
|
|
|
6817
|
|
|
// Iterates through event segments. Goes through all by default. |
6818
|
|
|
// If the optional `event` argument is specified, only iterates through segments linked to that event. |
6819
|
|
|
// The `this` value of the callback function will be the view. |
6820
|
|
|
segEach: function(func, event) { |
6821
|
|
|
var segs = this.getSegs(); |
6822
|
|
|
var i; |
6823
|
|
|
|
6824
|
|
|
for (i = 0; i < segs.length; i++) { |
6825
|
|
|
if (!event || segs[i].event._id === event._id) { |
6826
|
|
|
func.call(this, segs[i]); |
6827
|
|
|
} |
6828
|
|
|
} |
6829
|
|
|
}, |
6830
|
|
|
|
6831
|
|
|
|
6832
|
|
|
// Retrieves all the rendered segment objects for the view |
6833
|
|
|
getSegs: function() { |
6834
|
|
|
// subclasses must implement |
6835
|
|
|
}, |
6836
|
|
|
|
6837
|
|
|
|
6838
|
|
|
/* Event Drag Visualization |
6839
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
6840
|
|
|
|
6841
|
|
|
|
6842
|
|
|
// Renders a visual indication of an event hovering over the specified date. |
6843
|
|
|
// `end` is a Moment and might be null. |
6844
|
|
|
// `seg` might be null. if specified, it is the segment object of the event being dragged. |
6845
|
|
|
// otherwise, an external event from outside the calendar is being dragged. |
6846
|
|
|
renderDrag: function(start, end, seg) { |
6847
|
|
|
// subclasses should implement |
6848
|
|
|
}, |
6849
|
|
|
|
6850
|
|
|
|
6851
|
|
|
// Unrenders a visual indication of event hovering |
6852
|
|
|
destroyDrag: function() { |
6853
|
|
|
// subclasses should implement |
6854
|
|
|
}, |
6855
|
|
|
|
6856
|
|
|
|
6857
|
|
|
// Handler for accepting externally dragged events being dropped in the view. |
6858
|
|
|
// Gets called when jqui's 'dragstart' is fired. |
6859
|
|
|
documentDragStart: function(ev, ui) { |
6860
|
|
|
var _this = this; |
6861
|
|
|
var dropDate = null; |
6862
|
|
|
var dragListener; |
6863
|
|
|
|
6864
|
|
|
if (this.opt('droppable')) { // only listen if this setting is on |
6865
|
|
|
|
6866
|
|
|
// listener that tracks mouse movement over date-associated pixel regions |
6867
|
|
|
dragListener = new DragListener(this.coordMap, { |
6868
|
|
|
cellOver: function(cell, date) { |
6869
|
|
|
dropDate = date; |
6870
|
|
|
_this.renderDrag(date); |
6871
|
|
|
}, |
6872
|
|
|
cellOut: function() { |
6873
|
|
|
dropDate = null; |
6874
|
|
|
_this.destroyDrag(); |
6875
|
|
|
} |
6876
|
|
|
}); |
6877
|
|
|
|
6878
|
|
|
// gets called, only once, when jqui drag is finished |
6879
|
|
|
$(document).one('dragstop', function(ev, ui) { |
6880
|
|
|
_this.destroyDrag(); |
6881
|
|
|
if (dropDate) { |
6882
|
|
|
_this.trigger('drop', ev.target, dropDate, ev, ui); |
6883
|
|
|
} |
6884
|
|
|
}); |
6885
|
|
|
|
6886
|
|
|
dragListener.startDrag(ev); // start listening immediately |
6887
|
|
|
} |
6888
|
|
|
}, |
6889
|
|
|
|
6890
|
|
|
|
6891
|
|
|
/* Selection |
6892
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
6893
|
|
|
|
6894
|
|
|
|
6895
|
|
|
// Selects a date range on the view. `start` and `end` are both Moments. |
6896
|
|
|
// `ev` is the native mouse event that begin the interaction. |
6897
|
|
|
select: function(start, end, ev) { |
6898
|
|
|
this.unselect(ev); |
6899
|
|
|
this.renderSelection(start, end); |
6900
|
|
|
this.reportSelection(start, end, ev); |
6901
|
|
|
}, |
6902
|
|
|
|
6903
|
|
|
|
6904
|
|
|
// Renders a visual indication of the selection |
6905
|
|
|
renderSelection: function(start, end) { |
6906
|
|
|
// subclasses should implement |
6907
|
|
|
}, |
6908
|
|
|
|
6909
|
|
|
|
6910
|
|
|
// Called when a new selection is made. Updates internal state and triggers handlers. |
6911
|
|
|
reportSelection: function(start, end, ev) { |
6912
|
|
|
this.isSelected = true; |
6913
|
|
|
this.trigger('select', null, start, end, ev); |
6914
|
|
|
}, |
6915
|
|
|
|
6916
|
|
|
|
6917
|
|
|
// Undoes a selection. updates in the internal state and triggers handlers. |
6918
|
|
|
// `ev` is the native mouse event that began the interaction. |
6919
|
|
|
unselect: function(ev) { |
6920
|
|
|
if (this.isSelected) { |
6921
|
|
|
this.isSelected = false; |
6922
|
|
|
this.destroySelection(); |
6923
|
|
|
this.trigger('unselect', null, ev); |
6924
|
|
|
} |
6925
|
|
|
}, |
6926
|
|
|
|
6927
|
|
|
|
6928
|
|
|
// Unrenders a visual indication of selection |
6929
|
|
|
destroySelection: function() { |
6930
|
|
|
// subclasses should implement |
6931
|
|
|
}, |
6932
|
|
|
|
6933
|
|
|
|
6934
|
|
|
// Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on |
6935
|
|
|
documentMousedown: function(ev) { |
6936
|
|
|
var ignore; |
6937
|
|
|
|
6938
|
|
|
// is there a selection, and has the user made a proper left click? |
6939
|
|
|
if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) { |
6940
|
|
|
|
6941
|
|
|
// only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element |
6942
|
|
|
ignore = this.opt('unselectCancel'); |
6943
|
|
|
if (!ignore || !$(ev.target).closest(ignore).length) { |
6944
|
|
|
this.unselect(ev); |
6945
|
|
|
} |
6946
|
|
|
} |
6947
|
|
|
} |
6948
|
|
|
|
6949
|
|
|
}; |
6950
|
|
|
|
6951
|
|
|
|
6952
|
|
|
// We are mixing JavaScript OOP design patterns here by putting methods and member variables in the closed scope of the |
6953
|
|
|
// constructor. Going forward, methods should be part of the prototype. |
6954
|
|
|
function View(calendar) { |
6955
|
|
|
var t = this; |
6956
|
|
|
|
6957
|
|
|
// exports |
6958
|
|
|
t.calendar = calendar; |
6959
|
|
|
t.opt = opt; |
6960
|
|
|
t.trigger = trigger; |
6961
|
|
|
t.isEventDraggable = isEventDraggable; |
6962
|
|
|
t.isEventResizable = isEventResizable; |
6963
|
|
|
t.eventDrop = eventDrop; |
6964
|
|
|
t.eventResize = eventResize; |
6965
|
|
|
|
6966
|
|
|
// imports |
6967
|
|
|
var reportEventChange = calendar.reportEventChange; |
6968
|
|
|
|
6969
|
|
|
// locals |
6970
|
|
|
var options = calendar.options; |
6971
|
|
|
var nextDayThreshold = moment.duration(options.nextDayThreshold); |
6972
|
|
|
|
6973
|
|
|
|
6974
|
|
|
t.init(); // the "constructor" that concerns the prototype methods |
6975
|
|
|
|
6976
|
|
|
|
6977
|
|
|
function opt(name) { |
6978
|
|
|
var v = options[name]; |
6979
|
|
|
if ($.isPlainObject(v) && !isForcedAtomicOption(name)) { |
6980
|
|
|
return smartProperty(v, t.name); |
6981
|
|
|
} |
6982
|
|
|
return v; |
6983
|
|
|
} |
6984
|
|
|
|
6985
|
|
|
|
6986
|
|
|
function trigger(name, thisObj) { |
6987
|
|
|
return calendar.trigger.apply( |
6988
|
|
|
calendar, |
6989
|
|
|
[name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t]) |
6990
|
|
|
); |
6991
|
|
|
} |
6992
|
|
|
|
6993
|
|
|
|
6994
|
|
|
|
6995
|
|
|
/* Event Editable Boolean Calculations |
6996
|
|
|
------------------------------------------------------------------------------*/ |
6997
|
|
|
|
6998
|
|
|
|
6999
|
|
|
function isEventDraggable(event) { |
7000
|
|
|
var source = event.source || {}; |
7001
|
|
|
|
7002
|
|
|
return firstDefined( |
7003
|
|
|
event.startEditable, |
7004
|
|
|
source.startEditable, |
7005
|
|
|
opt('eventStartEditable'), |
7006
|
|
|
event.editable, |
7007
|
|
|
source.editable, |
7008
|
|
|
opt('editable') |
7009
|
|
|
); |
7010
|
|
|
} |
7011
|
|
|
|
7012
|
|
|
|
7013
|
|
|
function isEventResizable(event) { |
7014
|
|
|
var source = event.source || {}; |
7015
|
|
|
|
7016
|
|
|
return firstDefined( |
7017
|
|
|
event.durationEditable, |
7018
|
|
|
source.durationEditable, |
7019
|
|
|
opt('eventDurationEditable'), |
7020
|
|
|
event.editable, |
7021
|
|
|
source.editable, |
7022
|
|
|
opt('editable') |
7023
|
|
|
); |
7024
|
|
|
} |
7025
|
|
|
|
7026
|
|
|
|
7027
|
|
|
|
7028
|
|
|
/* Event Elements |
7029
|
|
|
------------------------------------------------------------------------------*/ |
7030
|
|
|
|
7031
|
|
|
|
7032
|
|
|
// Compute the text that should be displayed on an event's element. |
7033
|
|
|
// Based off the settings of the view. Possible signatures: |
7034
|
|
|
// .getEventTimeText(event, formatStr) |
7035
|
|
|
// .getEventTimeText(startMoment, endMoment, formatStr) |
7036
|
|
|
// .getEventTimeText(startMoment, null, formatStr) |
7037
|
|
|
// `timeFormat` is used but the `formatStr` argument can be used to override. |
7038
|
|
|
t.getEventTimeText = function(event, formatStr) { |
7039
|
|
|
var start; |
7040
|
|
|
var end; |
7041
|
|
|
|
7042
|
|
|
if (typeof event === 'object' && typeof formatStr === 'object') { |
7043
|
|
|
// first two arguments are actually moments (or null). shift arguments. |
7044
|
|
|
start = event; |
7045
|
|
|
end = formatStr; |
7046
|
|
|
formatStr = arguments[2]; |
7047
|
|
|
} |
7048
|
|
|
else { |
7049
|
|
|
// otherwise, an event object was the first argument |
7050
|
|
|
start = event.start; |
7051
|
|
|
end = event.end; |
7052
|
|
|
} |
7053
|
|
|
|
7054
|
|
|
formatStr = formatStr || opt('timeFormat'); |
7055
|
|
|
|
7056
|
|
|
if (end && opt('displayEventEnd')) { |
7057
|
|
|
return calendar.formatRange(start, end, formatStr); |
7058
|
|
|
} |
7059
|
|
|
else { |
7060
|
|
|
return calendar.formatDate(start, formatStr); |
7061
|
|
|
} |
7062
|
|
|
}; |
7063
|
|
|
|
7064
|
|
|
|
7065
|
|
|
|
7066
|
|
|
/* Event Modification Reporting |
7067
|
|
|
---------------------------------------------------------------------------------*/ |
7068
|
|
|
|
7069
|
|
|
|
7070
|
|
|
function eventDrop(el, event, newStart, ev) { |
7071
|
|
|
var mutateResult = calendar.mutateEvent(event, newStart, null); |
7072
|
|
|
|
7073
|
|
|
trigger( |
7074
|
|
|
'eventDrop', |
7075
|
|
|
el, |
7076
|
|
|
event, |
7077
|
|
|
mutateResult.dateDelta, |
7078
|
|
|
function() { |
7079
|
|
|
mutateResult.undo(); |
7080
|
|
|
reportEventChange(); |
7081
|
|
|
}, |
7082
|
|
|
ev, |
7083
|
|
|
{} // jqui dummy |
7084
|
|
|
); |
7085
|
|
|
|
7086
|
|
|
reportEventChange(); |
7087
|
|
|
} |
7088
|
|
|
|
7089
|
|
|
|
7090
|
|
|
function eventResize(el, event, newEnd, ev) { |
7091
|
|
|
var mutateResult = calendar.mutateEvent(event, null, newEnd); |
7092
|
|
|
|
7093
|
|
|
trigger( |
7094
|
|
|
'eventResize', |
7095
|
|
|
el, |
7096
|
|
|
event, |
7097
|
|
|
mutateResult.durationDelta, |
7098
|
|
|
function() { |
7099
|
|
|
mutateResult.undo(); |
7100
|
|
|
reportEventChange(); |
7101
|
|
|
}, |
7102
|
|
|
ev, |
7103
|
|
|
{} // jqui dummy |
7104
|
|
|
); |
7105
|
|
|
|
7106
|
|
|
reportEventChange(); |
7107
|
|
|
} |
7108
|
|
|
|
7109
|
|
|
|
7110
|
|
|
// ==================================================================================================== |
7111
|
|
|
// Utilities for day "cells" |
7112
|
|
|
// ==================================================================================================== |
7113
|
|
|
// The "basic" views are completely made up of day cells. |
7114
|
|
|
// The "agenda" views have day cells at the top "all day" slot. |
7115
|
|
|
// This was the obvious common place to put these utilities, but they should be abstracted out into |
7116
|
|
|
// a more meaningful class (like DayEventRenderer). |
7117
|
|
|
// ==================================================================================================== |
7118
|
|
|
|
7119
|
|
|
|
7120
|
|
|
// For determining how a given "cell" translates into a "date": |
7121
|
|
|
// |
7122
|
|
|
// 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first). |
7123
|
|
|
// Keep in mind that column indices are inverted with isRTL. This is taken into account. |
7124
|
|
|
// |
7125
|
|
|
// 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view). |
7126
|
|
|
// |
7127
|
|
|
// 3. Convert the "day offset" into a "date" (a Moment). |
7128
|
|
|
// |
7129
|
|
|
// The reverse transformation happens when transforming a date into a cell. |
7130
|
|
|
|
7131
|
|
|
|
7132
|
|
|
// exports |
7133
|
|
|
t.isHiddenDay = isHiddenDay; |
7134
|
|
|
t.skipHiddenDays = skipHiddenDays; |
7135
|
|
|
t.getCellsPerWeek = getCellsPerWeek; |
7136
|
|
|
t.dateToCell = dateToCell; |
7137
|
|
|
t.dateToDayOffset = dateToDayOffset; |
7138
|
|
|
t.dayOffsetToCellOffset = dayOffsetToCellOffset; |
7139
|
|
|
t.cellOffsetToCell = cellOffsetToCell; |
7140
|
|
|
t.cellToDate = cellToDate; |
7141
|
|
|
t.cellToCellOffset = cellToCellOffset; |
7142
|
|
|
t.cellOffsetToDayOffset = cellOffsetToDayOffset; |
7143
|
|
|
t.dayOffsetToDate = dayOffsetToDate; |
7144
|
|
|
t.rangeToSegments = rangeToSegments; |
7145
|
|
|
t.isMultiDayEvent = isMultiDayEvent; |
7146
|
|
|
|
7147
|
|
|
|
7148
|
|
|
// internals |
7149
|
|
|
var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden |
7150
|
|
|
var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) |
7151
|
|
|
var cellsPerWeek; |
7152
|
|
|
var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week |
7153
|
|
|
var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week |
7154
|
|
|
var isRTL = opt('isRTL'); |
7155
|
|
|
|
7156
|
|
|
|
7157
|
|
|
// initialize important internal variables |
7158
|
|
|
(function() { |
7159
|
|
|
|
7160
|
|
|
if (opt('weekends') === false) { |
7161
|
|
|
hiddenDays.push(0, 6); // 0=sunday, 6=saturday |
7162
|
|
|
} |
7163
|
|
|
|
7164
|
|
|
// Loop through a hypothetical week and determine which |
7165
|
|
|
// days-of-week are hidden. Record in both hashes (one is the reverse of the other). |
7166
|
|
|
for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) { |
7167
|
|
|
dayToCellMap[dayIndex] = cellIndex; |
7168
|
|
|
isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1; |
7169
|
|
|
if (!isHiddenDayHash[dayIndex]) { |
7170
|
|
|
cellToDayMap[cellIndex] = dayIndex; |
7171
|
|
|
cellIndex++; |
7172
|
|
|
} |
7173
|
|
|
} |
7174
|
|
|
|
7175
|
|
|
cellsPerWeek = cellIndex; |
7176
|
|
|
if (!cellsPerWeek) { |
7177
|
|
|
throw 'invalid hiddenDays'; // all days were hidden? bad. |
7178
|
|
|
} |
7179
|
|
|
|
7180
|
|
|
})(); |
7181
|
|
|
|
7182
|
|
|
|
7183
|
|
|
// Is the current day hidden? |
7184
|
|
|
// `day` is a day-of-week index (0-6), or a Moment |
7185
|
|
|
function isHiddenDay(day) { |
7186
|
|
|
if (moment.isMoment(day)) { |
7187
|
|
|
day = day.day(); |
7188
|
|
|
} |
7189
|
|
|
return isHiddenDayHash[day]; |
7190
|
|
|
} |
7191
|
|
|
|
7192
|
|
|
|
7193
|
|
|
function getCellsPerWeek() { |
7194
|
|
|
return cellsPerWeek; |
7195
|
|
|
} |
7196
|
|
|
|
7197
|
|
|
|
7198
|
|
|
// Incrementing the current day until it is no longer a hidden day, returning a copy. |
7199
|
|
|
// If the initial value of `date` is not a hidden day, don't do anything. |
7200
|
|
|
// Pass `isExclusive` as `true` if you are dealing with an end date. |
7201
|
|
|
// `inc` defaults to `1` (increment one day forward each time) |
7202
|
|
|
function skipHiddenDays(date, inc, isExclusive) { |
7203
|
|
|
var out = date.clone(); |
7204
|
|
|
inc = inc || 1; |
7205
|
|
|
while ( |
7206
|
|
|
isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7] |
7207
|
|
|
) { |
7208
|
|
|
out.add(inc, 'days'); |
7209
|
|
|
} |
7210
|
|
|
return out; |
7211
|
|
|
} |
7212
|
|
|
|
7213
|
|
|
|
7214
|
|
|
// |
7215
|
|
|
// TRANSFORMATIONS: cell -> cell offset -> day offset -> date |
7216
|
|
|
// |
7217
|
|
|
|
7218
|
|
|
// cell -> date (combines all transformations) |
7219
|
|
|
// Possible arguments: |
7220
|
|
|
// - row, col |
7221
|
|
|
// - { row:#, col: # } |
7222
|
|
|
function cellToDate() { |
7223
|
|
|
var cellOffset = cellToCellOffset.apply(null, arguments); |
7224
|
|
|
var dayOffset = cellOffsetToDayOffset(cellOffset); |
7225
|
|
|
var date = dayOffsetToDate(dayOffset); |
7226
|
|
|
return date; |
7227
|
|
|
} |
7228
|
|
|
|
7229
|
|
|
// cell -> cell offset |
7230
|
|
|
// Possible arguments: |
7231
|
|
|
// - row, col |
7232
|
|
|
// - { row:#, col:# } |
7233
|
|
|
function cellToCellOffset(row, col) { |
7234
|
|
|
var colCnt = t.colCnt; |
7235
|
|
|
|
7236
|
|
|
// rtl variables. wish we could pre-populate these. but where? |
7237
|
|
|
var dis = isRTL ? -1 : 1; |
7238
|
|
|
var dit = isRTL ? colCnt - 1 : 0; |
7239
|
|
|
|
7240
|
|
|
if (typeof row == 'object') { |
7241
|
|
|
col = row.col; |
7242
|
|
|
row = row.row; |
7243
|
|
|
} |
7244
|
|
|
var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit) |
7245
|
|
|
|
7246
|
|
|
return cellOffset; |
7247
|
|
|
} |
7248
|
|
|
|
7249
|
|
|
// cell offset -> day offset |
7250
|
|
|
function cellOffsetToDayOffset(cellOffset) { |
7251
|
|
|
var day0 = t.start.day(); // first date's day of week |
7252
|
|
|
cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week |
7253
|
|
|
return Math.floor(cellOffset / cellsPerWeek) * 7 + // # of days from full weeks |
7254
|
|
|
cellToDayMap[ // # of days from partial last week |
7255
|
|
|
(cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets |
7256
|
|
|
] - |
7257
|
|
|
day0; // adjustment for beginning-of-week normalization |
7258
|
|
|
} |
7259
|
|
|
|
7260
|
|
|
// day offset -> date |
7261
|
|
|
function dayOffsetToDate(dayOffset) { |
7262
|
|
|
return t.start.clone().add(dayOffset, 'days'); |
7263
|
|
|
} |
7264
|
|
|
|
7265
|
|
|
|
7266
|
|
|
// |
7267
|
|
|
// TRANSFORMATIONS: date -> day offset -> cell offset -> cell |
7268
|
|
|
// |
7269
|
|
|
|
7270
|
|
|
// date -> cell (combines all transformations) |
7271
|
|
|
function dateToCell(date) { |
7272
|
|
|
var dayOffset = dateToDayOffset(date); |
7273
|
|
|
var cellOffset = dayOffsetToCellOffset(dayOffset); |
7274
|
|
|
var cell = cellOffsetToCell(cellOffset); |
7275
|
|
|
return cell; |
7276
|
|
|
} |
7277
|
|
|
|
7278
|
|
|
// date -> day offset |
7279
|
|
|
function dateToDayOffset(date) { |
7280
|
|
|
return date.clone().stripTime().diff(t.start, 'days'); |
7281
|
|
|
} |
7282
|
|
|
|
7283
|
|
|
// day offset -> cell offset |
7284
|
|
|
function dayOffsetToCellOffset(dayOffset) { |
7285
|
|
|
var day0 = t.start.day(); // first date's day of week |
7286
|
|
|
dayOffset += day0; // normalize dayOffset to beginning-of-week |
7287
|
|
|
return Math.floor(dayOffset / 7) * cellsPerWeek + // # of cells from full weeks |
7288
|
|
|
dayToCellMap[ // # of cells from partial last week |
7289
|
|
|
(dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets |
7290
|
|
|
] - |
7291
|
|
|
dayToCellMap[day0]; // adjustment for beginning-of-week normalization |
7292
|
|
|
} |
7293
|
|
|
|
7294
|
|
|
// cell offset -> cell (object with row & col keys) |
7295
|
|
|
function cellOffsetToCell(cellOffset) { |
7296
|
|
|
var colCnt = t.colCnt; |
7297
|
|
|
|
7298
|
|
|
// rtl variables. wish we could pre-populate these. but where? |
7299
|
|
|
var dis = isRTL ? -1 : 1; |
7300
|
|
|
var dit = isRTL ? colCnt - 1 : 0; |
7301
|
|
|
|
7302
|
|
|
var row = Math.floor(cellOffset / colCnt); |
7303
|
|
|
var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit) |
7304
|
|
|
return { |
7305
|
|
|
row: row, |
7306
|
|
|
col: col |
7307
|
|
|
}; |
7308
|
|
|
} |
7309
|
|
|
|
7310
|
|
|
|
7311
|
|
|
// |
7312
|
|
|
// Converts a date range into an array of segment objects. |
7313
|
|
|
// "Segments" are horizontal stretches of time, sliced up by row. |
7314
|
|
|
// A segment object has the following properties: |
7315
|
|
|
// - row |
7316
|
|
|
// - cols |
7317
|
|
|
// - isStart |
7318
|
|
|
// - isEnd |
7319
|
|
|
// |
7320
|
|
|
function rangeToSegments(start, end) { |
7321
|
|
|
|
7322
|
|
|
var rowCnt = t.rowCnt; |
7323
|
|
|
var colCnt = t.colCnt; |
7324
|
|
|
var segments = []; // array of segments to return |
7325
|
|
|
|
7326
|
|
|
// day offset for given date range |
7327
|
|
|
var dayRange = computeDayRange(start, end); // convert to a whole-day range |
7328
|
|
|
var rangeDayOffsetStart = dateToDayOffset(dayRange.start); |
7329
|
|
|
var rangeDayOffsetEnd = dateToDayOffset(dayRange.end); // an exclusive value |
7330
|
|
|
|
7331
|
|
|
// first and last cell offset for the given date range |
7332
|
|
|
// "last" implies inclusivity |
7333
|
|
|
var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart); |
7334
|
|
|
var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1; |
7335
|
|
|
|
7336
|
|
|
// loop through all the rows in the view |
7337
|
|
|
for (var row=0; row<rowCnt; row++) { |
7338
|
|
|
|
7339
|
|
|
// first and last cell offset for the row |
7340
|
|
|
var rowCellOffsetFirst = row * colCnt; |
7341
|
|
|
var rowCellOffsetLast = rowCellOffsetFirst + colCnt - 1; |
7342
|
|
|
|
7343
|
|
|
// get the segment's cell offsets by constraining the range's cell offsets to the bounds of the row |
7344
|
|
|
var segmentCellOffsetFirst = Math.max(rangeCellOffsetFirst, rowCellOffsetFirst); |
7345
|
|
|
var segmentCellOffsetLast = Math.min(rangeCellOffsetLast, rowCellOffsetLast); |
7346
|
|
|
|
7347
|
|
|
// make sure segment's offsets are valid and in view |
7348
|
|
|
if (segmentCellOffsetFirst <= segmentCellOffsetLast) { |
7349
|
|
|
|
7350
|
|
|
// translate to cells |
7351
|
|
|
var segmentCellFirst = cellOffsetToCell(segmentCellOffsetFirst); |
7352
|
|
|
var segmentCellLast = cellOffsetToCell(segmentCellOffsetLast); |
7353
|
|
|
|
7354
|
|
|
// view might be RTL, so order by leftmost column |
7355
|
|
|
var cols = [ segmentCellFirst.col, segmentCellLast.col ].sort(); |
7356
|
|
|
|
7357
|
|
|
// Determine if segment's first/last cell is the beginning/end of the date range. |
7358
|
|
|
// We need to compare "day offset" because "cell offsets" are often ambiguous and |
7359
|
|
|
// can translate to multiple days, and an edge case reveals itself when we the |
7360
|
|
|
// range's first cell is hidden (we don't want isStart to be true). |
7361
|
|
|
var isStart = cellOffsetToDayOffset(segmentCellOffsetFirst) == rangeDayOffsetStart; |
7362
|
|
|
var isEnd = cellOffsetToDayOffset(segmentCellOffsetLast) + 1 == rangeDayOffsetEnd; |
7363
|
|
|
// +1 for comparing exclusively |
7364
|
|
|
|
7365
|
|
|
segments.push({ |
7366
|
|
|
row: row, |
7367
|
|
|
leftCol: cols[0], |
7368
|
|
|
rightCol: cols[1], |
7369
|
|
|
isStart: isStart, |
7370
|
|
|
isEnd: isEnd |
7371
|
|
|
}); |
7372
|
|
|
} |
7373
|
|
|
} |
7374
|
|
|
|
7375
|
|
|
return segments; |
7376
|
|
|
} |
7377
|
|
|
|
7378
|
|
|
|
7379
|
|
|
// Returns the date range of the full days the given range visually appears to occupy. |
7380
|
|
|
// Returns object with properties `start` (moment) and `end` (moment, exclusive end). |
7381
|
|
|
function computeDayRange(start, end) { |
7382
|
|
|
var startDay = start.clone().stripTime(); // the beginning of the day the range starts |
7383
|
|
|
var endDay; |
7384
|
|
|
var endTimeMS; |
7385
|
|
|
|
7386
|
|
|
if (end) { |
7387
|
|
|
endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends |
7388
|
|
|
endTimeMS = +end.time(); // # of milliseconds into `endDay` |
7389
|
|
|
|
7390
|
|
|
// If the end time is actually inclusively part of the next day and is equal to or |
7391
|
|
|
// beyond the next day threshold, adjust the end to be the exclusive end of `endDay`. |
7392
|
|
|
// Otherwise, leaving it as inclusive will cause it to exclude `endDay`. |
7393
|
|
|
if (endTimeMS && endTimeMS >= nextDayThreshold) { |
7394
|
|
|
endDay.add(1, 'days'); |
7395
|
|
|
} |
7396
|
|
|
} |
7397
|
|
|
|
7398
|
|
|
// If no end was specified, or if it is within `startDay` but not past nextDayThreshold, |
7399
|
|
|
// assign the default duration of one day. |
7400
|
|
|
if (!end || endDay <= startDay) { |
|
|
|
|
7401
|
|
|
endDay = startDay.clone().add(1, 'days'); |
7402
|
|
|
} |
7403
|
|
|
|
7404
|
|
|
return { start: startDay, end: endDay }; |
7405
|
|
|
} |
7406
|
|
|
|
7407
|
|
|
|
7408
|
|
|
// Does the given event visually appear to occupy more than one day? |
7409
|
|
|
function isMultiDayEvent(event) { |
7410
|
|
|
var range = computeDayRange(event.start, event.end); |
7411
|
|
|
|
7412
|
|
|
return range.end.diff(range.start, 'days') > 1; |
7413
|
|
|
} |
7414
|
|
|
|
7415
|
|
|
} |
7416
|
|
|
|
7417
|
|
|
;; |
7418
|
|
|
|
7419
|
|
|
/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells. |
7420
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
7421
|
|
|
// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting. |
7422
|
|
|
// It is responsible for managing width/height. |
7423
|
|
|
|
7424
|
|
|
function BasicView(calendar) { |
7425
|
|
|
View.call(this, calendar); // call the super-constructor |
7426
|
|
|
this.dayGrid = new DayGrid(this); |
7427
|
|
|
this.coordMap = this.dayGrid.coordMap; // the view's date-to-cell mapping is identical to the subcomponent's |
7428
|
|
|
} |
7429
|
|
|
|
7430
|
|
|
|
7431
|
|
|
BasicView.prototype = createObject(View.prototype); // define the super-class |
7432
|
|
|
$.extend(BasicView.prototype, { |
7433
|
|
|
|
7434
|
|
|
dayGrid: null, // the main subcomponent that does most of the heavy lifting |
7435
|
|
|
|
7436
|
|
|
dayNumbersVisible: false, // display day numbers on each day cell? |
7437
|
|
|
weekNumbersVisible: false, // display week numbers along the side? |
7438
|
|
|
|
7439
|
|
|
weekNumberWidth: null, // width of all the week-number cells running down the side |
7440
|
|
|
|
7441
|
|
|
headRowEl: null, // the fake row element of the day-of-week header |
7442
|
|
|
|
7443
|
|
|
|
7444
|
|
|
// Renders the view into `this.el`, which should already be assigned. |
7445
|
|
|
// rowCnt, colCnt, and dayNumbersVisible have been calculated by a subclass and passed here. |
7446
|
|
|
render: function(rowCnt, colCnt, dayNumbersVisible) { |
7447
|
|
|
|
7448
|
|
|
// needed for cell-to-date and date-to-cell calculations in View |
7449
|
|
|
this.rowCnt = rowCnt; |
7450
|
|
|
this.colCnt = colCnt; |
7451
|
|
|
|
7452
|
|
|
this.dayNumbersVisible = dayNumbersVisible; |
7453
|
|
|
this.weekNumbersVisible = this.opt('weekNumbers'); |
7454
|
|
|
this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible; |
7455
|
|
|
|
7456
|
|
|
this.el.addClass('fc-basic-view').html(this.renderHtml()); |
7457
|
|
|
|
7458
|
|
|
this.headRowEl = this.el.find('thead .fc-row'); |
7459
|
|
|
|
7460
|
|
|
this.scrollerEl = this.el.find('.fc-day-grid-container'); |
7461
|
|
|
this.dayGrid.coordMap.containerEl = this.scrollerEl; // constrain clicks/etc to the dimensions of the scroller |
7462
|
|
|
|
7463
|
|
|
this.dayGrid.el = this.el.find('.fc-day-grid'); |
7464
|
|
|
this.dayGrid.render(this.hasRigidRows()); |
7465
|
|
|
|
7466
|
|
|
View.prototype.render.call(this); // call the super-method |
7467
|
|
|
}, |
7468
|
|
|
|
7469
|
|
|
|
7470
|
|
|
// Make subcomponents ready for cleanup |
7471
|
|
|
destroy: function() { |
7472
|
|
|
this.dayGrid.destroy(); |
7473
|
|
|
View.prototype.destroy.call(this); // call the super-method |
7474
|
|
|
}, |
7475
|
|
|
|
7476
|
|
|
|
7477
|
|
|
// Builds the HTML skeleton for the view. |
7478
|
|
|
// The day-grid component will render inside of a container defined by this HTML. |
7479
|
|
|
renderHtml: function() { |
7480
|
|
|
return '' + |
7481
|
|
|
'<table>' + |
7482
|
|
|
'<thead>' + |
7483
|
|
|
'<tr>' + |
7484
|
|
|
'<td class="' + this.widgetHeaderClass + '">' + |
7485
|
|
|
this.dayGrid.headHtml() + // render the day-of-week headers |
7486
|
|
|
'</td>' + |
7487
|
|
|
'</tr>' + |
7488
|
|
|
'</thead>' + |
7489
|
|
|
'<tbody>' + |
7490
|
|
|
'<tr>' + |
7491
|
|
|
'<td class="' + this.widgetContentClass + '">' + |
7492
|
|
|
'<div class="fc-day-grid-container">' + |
7493
|
|
|
'<div class="fc-day-grid"/>' + |
7494
|
|
|
'</div>' + |
7495
|
|
|
'</td>' + |
7496
|
|
|
'</tr>' + |
7497
|
|
|
'</tbody>' + |
7498
|
|
|
'</table>'; |
7499
|
|
|
}, |
7500
|
|
|
|
7501
|
|
|
|
7502
|
|
|
// Generates the HTML that will go before the day-of week header cells. |
7503
|
|
|
// Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL. |
7504
|
|
|
headIntroHtml: function() { |
7505
|
|
|
if (this.weekNumbersVisible) { |
7506
|
|
|
return '' + |
7507
|
|
|
'<th class="fc-week-number ' + this.widgetHeaderClass + '" ' + this.weekNumberStyleAttr() + '>' + |
7508
|
|
|
'<span>' + // needed for matchCellWidths |
7509
|
|
|
htmlEscape(this.opt('weekNumberTitle')) + |
7510
|
|
|
'</span>' + |
7511
|
|
|
'</th>'; |
7512
|
|
|
} |
7513
|
|
|
}, |
7514
|
|
|
|
7515
|
|
|
|
7516
|
|
|
// Generates the HTML that will go before content-skeleton cells that display the day/week numbers. |
7517
|
|
|
// Queried by the DayGrid subcomponent. Ordering depends on isRTL. |
7518
|
|
|
numberIntroHtml: function(row) { |
7519
|
|
|
if (this.weekNumbersVisible) { |
7520
|
|
|
return '' + |
7521
|
|
|
'<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '>' + |
7522
|
|
|
'<span>' + // needed for matchCellWidths |
7523
|
|
|
this.calendar.calculateWeekNumber(this.cellToDate(row, 0)) + |
7524
|
|
|
'</span>' + |
7525
|
|
|
'</td>'; |
7526
|
|
|
} |
7527
|
|
|
}, |
7528
|
|
|
|
7529
|
|
|
|
7530
|
|
|
// Generates the HTML that goes before the day bg cells for each day-row. |
7531
|
|
|
// Queried by the DayGrid subcomponent. Ordering depends on isRTL. |
7532
|
|
|
dayIntroHtml: function() { |
7533
|
|
|
if (this.weekNumbersVisible) { |
7534
|
|
|
return '<td class="fc-week-number ' + this.widgetContentClass + '" ' + |
7535
|
|
|
this.weekNumberStyleAttr() + '></td>'; |
7536
|
|
|
} |
7537
|
|
|
}, |
7538
|
|
|
|
7539
|
|
|
|
7540
|
|
|
// Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL. |
7541
|
|
|
// Affects helper-skeleton and highlight-skeleton rows. |
7542
|
|
|
introHtml: function() { |
7543
|
|
|
if (this.weekNumbersVisible) { |
7544
|
|
|
return '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '></td>'; |
7545
|
|
|
} |
7546
|
|
|
}, |
7547
|
|
|
|
7548
|
|
|
|
7549
|
|
|
// Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton. |
7550
|
|
|
// The number row will only exist if either day numbers or week numbers are turned on. |
7551
|
|
|
numberCellHtml: function(row, col, date) { |
7552
|
|
|
var classes; |
7553
|
|
|
|
7554
|
|
|
if (!this.dayNumbersVisible) { // if there are week numbers but not day numbers |
7555
|
|
|
return '<td/>'; // will create an empty space above events :( |
7556
|
|
|
} |
7557
|
|
|
|
7558
|
|
|
classes = this.dayGrid.getDayClasses(date); |
7559
|
|
|
classes.unshift('fc-day-number'); |
7560
|
|
|
|
7561
|
|
|
return '' + |
7562
|
|
|
'<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">' + |
7563
|
|
|
date.date() + |
7564
|
|
|
'</td>'; |
7565
|
|
|
}, |
7566
|
|
|
|
7567
|
|
|
|
7568
|
|
|
// Generates an HTML attribute string for setting the width of the week number column, if it is known |
7569
|
|
|
weekNumberStyleAttr: function() { |
7570
|
|
|
if (this.weekNumberWidth !== null) { |
7571
|
|
|
return 'style="width:' + this.weekNumberWidth + 'px"'; |
7572
|
|
|
} |
7573
|
|
|
return ''; |
7574
|
|
|
}, |
7575
|
|
|
|
7576
|
|
|
|
7577
|
|
|
// Determines whether each row should have a constant height |
7578
|
|
|
hasRigidRows: function() { |
7579
|
|
|
var eventLimit = this.opt('eventLimit'); |
7580
|
|
|
return eventLimit && typeof eventLimit !== 'number'; |
7581
|
|
|
}, |
7582
|
|
|
|
7583
|
|
|
|
7584
|
|
|
/* Dimensions |
7585
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
7586
|
|
|
|
7587
|
|
|
|
7588
|
|
|
// Refreshes the horizontal dimensions of the view |
7589
|
|
|
updateWidth: function() { |
7590
|
|
|
if (this.weekNumbersVisible) { |
7591
|
|
|
// Make sure all week number cells running down the side have the same width. |
7592
|
|
|
// Record the width for cells created later. |
7593
|
|
|
this.weekNumberWidth = matchCellWidths( |
7594
|
|
|
this.el.find('.fc-week-number') |
7595
|
|
|
); |
7596
|
|
|
} |
7597
|
|
|
}, |
7598
|
|
|
|
7599
|
|
|
|
7600
|
|
|
// Adjusts the vertical dimensions of the view to the specified values |
7601
|
|
|
setHeight: function(totalHeight, isAuto) { |
7602
|
|
|
var eventLimit = this.opt('eventLimit'); |
7603
|
|
|
var scrollerHeight; |
7604
|
|
|
|
7605
|
|
|
// reset all heights to be natural |
7606
|
|
|
unsetScroller(this.scrollerEl); |
7607
|
|
|
uncompensateScroll(this.headRowEl); |
7608
|
|
|
|
7609
|
|
|
this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed |
7610
|
|
|
|
7611
|
|
|
// is the event limit a constant level number? |
7612
|
|
|
if (eventLimit && typeof eventLimit === 'number') { |
7613
|
|
|
this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after |
7614
|
|
|
} |
7615
|
|
|
|
7616
|
|
|
scrollerHeight = this.computeScrollerHeight(totalHeight); |
7617
|
|
|
this.setGridHeight(scrollerHeight, isAuto); |
7618
|
|
|
|
7619
|
|
|
// is the event limit dynamically calculated? |
7620
|
|
|
if (eventLimit && typeof eventLimit !== 'number') { |
7621
|
|
|
this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set |
7622
|
|
|
} |
7623
|
|
|
|
7624
|
|
|
if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? |
7625
|
|
|
|
7626
|
|
|
compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl)); |
7627
|
|
|
|
7628
|
|
|
// doing the scrollbar compensation might have created text overflow which created more height. redo |
7629
|
|
|
scrollerHeight = this.computeScrollerHeight(totalHeight); |
7630
|
|
|
this.scrollerEl.height(scrollerHeight); |
7631
|
|
|
|
7632
|
|
|
this.restoreScroll(); |
7633
|
|
|
} |
7634
|
|
|
}, |
7635
|
|
|
|
7636
|
|
|
|
7637
|
|
|
// Sets the height of just the DayGrid component in this view |
7638
|
|
|
setGridHeight: function(height, isAuto) { |
7639
|
|
|
if (isAuto) { |
7640
|
|
|
undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding |
7641
|
|
|
} |
7642
|
|
|
else { |
7643
|
|
|
distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows |
7644
|
|
|
} |
7645
|
|
|
}, |
7646
|
|
|
|
7647
|
|
|
|
7648
|
|
|
/* Events |
7649
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
7650
|
|
|
|
7651
|
|
|
|
7652
|
|
|
// Renders the given events onto the view and populates the segments array |
7653
|
|
|
renderEvents: function(events) { |
7654
|
|
|
this.dayGrid.renderEvents(events); |
7655
|
|
|
|
7656
|
|
|
this.updateHeight(); // must compensate for events that overflow the row |
7657
|
|
|
|
7658
|
|
|
View.prototype.renderEvents.call(this, events); // call the super-method |
7659
|
|
|
}, |
7660
|
|
|
|
7661
|
|
|
|
7662
|
|
|
// Retrieves all segment objects that are rendered in the view |
7663
|
|
|
getSegs: function() { |
7664
|
|
|
return this.dayGrid.getSegs(); |
7665
|
|
|
}, |
7666
|
|
|
|
7667
|
|
|
|
7668
|
|
|
// Unrenders all event elements and clears internal segment data |
7669
|
|
|
destroyEvents: function() { |
7670
|
|
|
View.prototype.destroyEvents.call(this); // do this before dayGrid's segs have been cleared |
7671
|
|
|
|
7672
|
|
|
this.recordScroll(); // removing events will reduce height and mess with the scroll, so record beforehand |
7673
|
|
|
this.dayGrid.destroyEvents(); |
7674
|
|
|
|
7675
|
|
|
// we DON'T need to call updateHeight() because: |
7676
|
|
|
// A) a renderEvents() call always happens after this, which will eventually call updateHeight() |
7677
|
|
|
// B) in IE8, this causes a flash whenever events are rerendered |
7678
|
|
|
}, |
7679
|
|
|
|
7680
|
|
|
|
7681
|
|
|
/* Event Dragging |
7682
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
7683
|
|
|
|
7684
|
|
|
|
7685
|
|
|
// Renders a visual indication of an event being dragged over the view. |
7686
|
|
|
// A returned value of `true` signals that a mock "helper" event has been rendered. |
7687
|
|
|
renderDrag: function(start, end, seg) { |
7688
|
|
|
return this.dayGrid.renderDrag(start, end, seg); |
7689
|
|
|
}, |
7690
|
|
|
|
7691
|
|
|
|
7692
|
|
|
// Unrenders the visual indication of an event being dragged over the view |
7693
|
|
|
destroyDrag: function() { |
7694
|
|
|
this.dayGrid.destroyDrag(); |
7695
|
|
|
}, |
7696
|
|
|
|
7697
|
|
|
|
7698
|
|
|
/* Selection |
7699
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
7700
|
|
|
|
7701
|
|
|
|
7702
|
|
|
// Renders a visual indication of a selection |
7703
|
|
|
renderSelection: function(start, end) { |
7704
|
|
|
this.dayGrid.renderSelection(start, end); |
7705
|
|
|
}, |
7706
|
|
|
|
7707
|
|
|
|
7708
|
|
|
// Unrenders a visual indications of a selection |
7709
|
|
|
destroySelection: function() { |
7710
|
|
|
this.dayGrid.destroySelection(); |
7711
|
|
|
} |
7712
|
|
|
|
7713
|
|
|
}); |
7714
|
|
|
|
7715
|
|
|
;; |
7716
|
|
|
|
7717
|
|
|
/* A month view with day cells running in rows (one-per-week) and columns |
7718
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
7719
|
|
|
|
7720
|
|
|
setDefaults({ |
7721
|
|
|
fixedWeekCount: true |
7722
|
|
|
}); |
7723
|
|
|
|
7724
|
|
|
fcViews.month = MonthView; // register the view |
7725
|
|
|
|
7726
|
|
|
function MonthView(calendar) { |
7727
|
|
|
BasicView.call(this, calendar); // call the super-constructor |
7728
|
|
|
} |
7729
|
|
|
|
7730
|
|
|
|
7731
|
|
|
MonthView.prototype = createObject(BasicView.prototype); // define the super-class |
7732
|
|
|
$.extend(MonthView.prototype, { |
7733
|
|
|
|
7734
|
|
|
name: 'month', |
7735
|
|
|
|
7736
|
|
|
|
7737
|
|
|
incrementDate: function(date, delta) { |
7738
|
|
|
return date.clone().stripTime().add(delta, 'months').startOf('month'); |
7739
|
|
|
}, |
7740
|
|
|
|
7741
|
|
|
|
7742
|
|
|
render: function(date) { |
7743
|
|
|
var rowCnt; |
7744
|
|
|
|
7745
|
|
|
this.intervalStart = date.clone().stripTime().startOf('month'); |
7746
|
|
|
this.intervalEnd = this.intervalStart.clone().add(1, 'months'); |
7747
|
|
|
|
7748
|
|
|
this.start = this.intervalStart.clone(); |
7749
|
|
|
this.start = this.skipHiddenDays(this.start); // move past the first week if no visible days |
7750
|
|
|
this.start.startOf('week'); |
7751
|
|
|
this.start = this.skipHiddenDays(this.start); // move past the first invisible days of the week |
7752
|
|
|
|
7753
|
|
|
this.end = this.intervalEnd.clone(); |
7754
|
|
|
this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last week if no visible days |
7755
|
|
|
this.end.add((7 - this.end.weekday()) % 7, 'days'); // move to end of week if not already |
7756
|
|
|
this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last invisible days of the week |
7757
|
|
|
|
7758
|
|
|
rowCnt = Math.ceil( // need to ceil in case there are hidden days |
7759
|
|
|
this.end.diff(this.start, 'weeks', true) // returnfloat=true |
7760
|
|
|
); |
7761
|
|
|
if (this.isFixedWeeks()) { |
7762
|
|
|
this.end.add(6 - rowCnt, 'weeks'); |
7763
|
|
|
rowCnt = 6; |
7764
|
|
|
} |
7765
|
|
|
|
7766
|
|
|
this.title = this.calendar.formatDate(this.intervalStart, this.opt('titleFormat')); |
7767
|
|
|
|
7768
|
|
|
BasicView.prototype.render.call(this, rowCnt, this.getCellsPerWeek(), true); // call the super-method |
7769
|
|
|
}, |
7770
|
|
|
|
7771
|
|
|
|
7772
|
|
|
// Overrides the default BasicView behavior to have special multi-week auto-height logic |
7773
|
|
|
setGridHeight: function(height, isAuto) { |
7774
|
|
|
|
7775
|
|
|
isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated |
7776
|
|
|
|
7777
|
|
|
// if auto, make the height of each row the height that it would be if there were 6 weeks |
7778
|
|
|
if (isAuto) { |
7779
|
|
|
height *= this.rowCnt / 6; |
7780
|
|
|
} |
7781
|
|
|
|
7782
|
|
|
distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows |
7783
|
|
|
}, |
7784
|
|
|
|
7785
|
|
|
|
7786
|
|
|
isFixedWeeks: function() { |
7787
|
|
|
var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated |
7788
|
|
|
if (weekMode) { |
7789
|
|
|
return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed |
7790
|
|
|
} |
7791
|
|
|
|
7792
|
|
|
return this.opt('fixedWeekCount'); |
7793
|
|
|
} |
7794
|
|
|
|
7795
|
|
|
}); |
7796
|
|
|
|
7797
|
|
|
;; |
7798
|
|
|
|
7799
|
|
|
/* A week view with simple day cells running horizontally |
7800
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
7801
|
|
|
// TODO: a WeekView mixin for calculating dates and titles |
7802
|
|
|
|
7803
|
|
|
fcViews.basicWeek = BasicWeekView; // register this view |
7804
|
|
|
|
7805
|
|
|
function BasicWeekView(calendar) { |
7806
|
|
|
BasicView.call(this, calendar); // call the super-constructor |
7807
|
|
|
} |
7808
|
|
|
|
7809
|
|
|
|
7810
|
|
|
BasicWeekView.prototype = createObject(BasicView.prototype); // define the super-class |
7811
|
|
|
$.extend(BasicWeekView.prototype, { |
7812
|
|
|
|
7813
|
|
|
name: 'basicWeek', |
7814
|
|
|
|
7815
|
|
|
|
7816
|
|
|
incrementDate: function(date, delta) { |
7817
|
|
|
return date.clone().stripTime().add(delta, 'weeks').startOf('week'); |
7818
|
|
|
}, |
7819
|
|
|
|
7820
|
|
|
|
7821
|
|
|
render: function(date) { |
7822
|
|
|
|
7823
|
|
|
this.intervalStart = date.clone().stripTime().startOf('week'); |
7824
|
|
|
this.intervalEnd = this.intervalStart.clone().add(1, 'weeks'); |
7825
|
|
|
|
7826
|
|
|
this.start = this.skipHiddenDays(this.intervalStart); |
7827
|
|
|
this.end = this.skipHiddenDays(this.intervalEnd, -1, true); |
7828
|
|
|
|
7829
|
|
|
this.title = this.calendar.formatRange( |
7830
|
|
|
this.start, |
7831
|
|
|
this.end.clone().subtract(1), // make inclusive by subtracting 1 ms |
7832
|
|
|
this.opt('titleFormat'), |
7833
|
|
|
' \u2014 ' // emphasized dash |
7834
|
|
|
); |
7835
|
|
|
|
7836
|
|
|
BasicView.prototype.render.call(this, 1, this.getCellsPerWeek(), false); // call the super-method |
7837
|
|
|
} |
7838
|
|
|
|
7839
|
|
|
}); |
7840
|
|
|
;; |
7841
|
|
|
|
7842
|
|
|
/* A view with a single simple day cell |
7843
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
7844
|
|
|
|
7845
|
|
|
fcViews.basicDay = BasicDayView; // register this view |
7846
|
|
|
|
7847
|
|
|
function BasicDayView(calendar) { |
7848
|
|
|
BasicView.call(this, calendar); // call the super-constructor |
7849
|
|
|
} |
7850
|
|
|
|
7851
|
|
|
|
7852
|
|
|
BasicDayView.prototype = createObject(BasicView.prototype); // define the super-class |
7853
|
|
|
$.extend(BasicDayView.prototype, { |
7854
|
|
|
|
7855
|
|
|
name: 'basicDay', |
7856
|
|
|
|
7857
|
|
|
|
7858
|
|
|
incrementDate: function(date, delta) { |
7859
|
|
|
var out = date.clone().stripTime().add(delta, 'days'); |
7860
|
|
|
out = this.skipHiddenDays(out, delta < 0 ? -1 : 1); |
7861
|
|
|
return out; |
7862
|
|
|
}, |
7863
|
|
|
|
7864
|
|
|
|
7865
|
|
|
render: function(date) { |
7866
|
|
|
|
7867
|
|
|
this.start = this.intervalStart = date.clone().stripTime(); |
7868
|
|
|
this.end = this.intervalEnd = this.start.clone().add(1, 'days'); |
7869
|
|
|
|
7870
|
|
|
this.title = this.calendar.formatDate(this.start, this.opt('titleFormat')); |
7871
|
|
|
|
7872
|
|
|
BasicView.prototype.render.call(this, 1, 1, false); // call the super-method |
7873
|
|
|
} |
7874
|
|
|
|
7875
|
|
|
}); |
7876
|
|
|
;; |
7877
|
|
|
|
7878
|
|
|
/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. |
7879
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
7880
|
|
|
// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). |
7881
|
|
|
// Responsible for managing width/height. |
7882
|
|
|
|
7883
|
|
|
setDefaults({ |
7884
|
|
|
allDaySlot: true, |
7885
|
|
|
allDayText: 'all-day', |
7886
|
|
|
|
7887
|
|
|
scrollTime: '06:00:00', |
7888
|
|
|
|
7889
|
|
|
slotDuration: '00:30:00', |
7890
|
|
|
|
7891
|
|
|
axisFormat: generateAgendaAxisFormat, |
7892
|
|
|
timeFormat: { |
7893
|
|
|
agenda: generateAgendaTimeFormat |
7894
|
|
|
}, |
7895
|
|
|
|
7896
|
|
|
minTime: '00:00:00', |
7897
|
|
|
maxTime: '24:00:00', |
7898
|
|
|
slotEventOverlap: true |
7899
|
|
|
}); |
7900
|
|
|
|
7901
|
|
|
var AGENDA_ALL_DAY_EVENT_LIMIT = 5; |
7902
|
|
|
|
7903
|
|
|
|
7904
|
|
|
function generateAgendaAxisFormat(options, langData) { |
7905
|
|
|
return langData.longDateFormat('LT') |
7906
|
|
|
.replace(':mm', '(:mm)') |
7907
|
|
|
.replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs |
7908
|
|
|
.replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand |
7909
|
|
|
} |
7910
|
|
|
|
7911
|
|
|
|
7912
|
|
|
function generateAgendaTimeFormat(options, langData) { |
7913
|
|
|
return langData.longDateFormat('LT') |
7914
|
|
|
.replace(/\s*a$/i, ''); // remove trailing AM/PM |
7915
|
|
|
} |
7916
|
|
|
|
7917
|
|
|
|
7918
|
|
|
function AgendaView(calendar) { |
7919
|
|
|
View.call(this, calendar); // call the super-constructor |
7920
|
|
|
|
7921
|
|
|
this.timeGrid = new TimeGrid(this); |
7922
|
|
|
|
7923
|
|
|
if (this.opt('allDaySlot')) { // should we display the "all-day" area? |
7924
|
|
|
this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view |
7925
|
|
|
|
7926
|
|
|
// the coordinate grid will be a combination of both subcomponents' grids |
7927
|
|
|
this.coordMap = new ComboCoordMap([ |
7928
|
|
|
this.dayGrid.coordMap, |
7929
|
|
|
this.timeGrid.coordMap |
7930
|
|
|
]); |
7931
|
|
|
} |
7932
|
|
|
else { |
7933
|
|
|
this.coordMap = this.timeGrid.coordMap; |
7934
|
|
|
} |
7935
|
|
|
} |
7936
|
|
|
|
7937
|
|
|
|
7938
|
|
|
AgendaView.prototype = createObject(View.prototype); // define the super-class |
7939
|
|
|
$.extend(AgendaView.prototype, { |
7940
|
|
|
|
7941
|
|
|
timeGrid: null, // the main time-grid subcomponent of this view |
7942
|
|
|
dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null |
7943
|
|
|
|
7944
|
|
|
axisWidth: null, // the width of the time axis running down the side |
7945
|
|
|
|
7946
|
|
|
noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars |
7947
|
|
|
|
7948
|
|
|
// when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath |
7949
|
|
|
bottomRuleEl: null, |
7950
|
|
|
bottomRuleHeight: null, |
7951
|
|
|
|
7952
|
|
|
|
7953
|
|
|
/* Rendering |
7954
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
7955
|
|
|
|
7956
|
|
|
|
7957
|
|
|
// Renders the view into `this.el`, which has already been assigned. |
7958
|
|
|
// `colCnt` has been calculated by a subclass and passed here. |
7959
|
|
|
render: function(colCnt) { |
7960
|
|
|
|
7961
|
|
|
// needed for cell-to-date and date-to-cell calculations in View |
7962
|
|
|
this.rowCnt = 1; |
7963
|
|
|
this.colCnt = colCnt; |
7964
|
|
|
|
7965
|
|
|
this.el.addClass('fc-agenda-view').html(this.renderHtml()); |
7966
|
|
|
|
7967
|
|
|
// the element that wraps the time-grid that will probably scroll |
7968
|
|
|
this.scrollerEl = this.el.find('.fc-time-grid-container'); |
7969
|
|
|
this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this |
7970
|
|
|
|
7971
|
|
|
this.timeGrid.el = this.el.find('.fc-time-grid'); |
7972
|
|
|
this.timeGrid.render(); |
7973
|
|
|
|
7974
|
|
|
// the <hr> that sometimes displays under the time-grid |
7975
|
|
|
this.bottomRuleEl = $('<hr class="' + this.widgetHeaderClass + '"/>') |
7976
|
|
|
.appendTo(this.timeGrid.el); // inject it into the time-grid |
7977
|
|
|
|
7978
|
|
|
if (this.dayGrid) { |
7979
|
|
|
this.dayGrid.el = this.el.find('.fc-day-grid'); |
7980
|
|
|
this.dayGrid.render(); |
7981
|
|
|
|
7982
|
|
|
// have the day-grid extend it's coordinate area over the <hr> dividing the two grids |
7983
|
|
|
this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); |
7984
|
|
|
} |
7985
|
|
|
|
7986
|
|
|
this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller |
7987
|
|
|
|
7988
|
|
|
View.prototype.render.call(this); // call the super-method |
7989
|
|
|
|
7990
|
|
|
this.resetScroll(); // do this after sizes have been set |
7991
|
|
|
}, |
7992
|
|
|
|
7993
|
|
|
|
7994
|
|
|
// Make subcomponents ready for cleanup |
7995
|
|
|
destroy: function() { |
7996
|
|
|
this.timeGrid.destroy(); |
7997
|
|
|
if (this.dayGrid) { |
7998
|
|
|
this.dayGrid.destroy(); |
7999
|
|
|
} |
8000
|
|
|
View.prototype.destroy.call(this); // call the super-method |
8001
|
|
|
}, |
8002
|
|
|
|
8003
|
|
|
|
8004
|
|
|
// Builds the HTML skeleton for the view. |
8005
|
|
|
// The day-grid and time-grid components will render inside containers defined by this HTML. |
8006
|
|
|
renderHtml: function() { |
8007
|
|
|
return '' + |
8008
|
|
|
'<table>' + |
8009
|
|
|
'<thead>' + |
8010
|
|
|
'<tr>' + |
8011
|
|
|
'<td class="' + this.widgetHeaderClass + '">' + |
8012
|
|
|
this.timeGrid.headHtml() + // render the day-of-week headers |
8013
|
|
|
'</td>' + |
8014
|
|
|
'</tr>' + |
8015
|
|
|
'</thead>' + |
8016
|
|
|
'<tbody>' + |
8017
|
|
|
'<tr>' + |
8018
|
|
|
'<td class="' + this.widgetContentClass + '">' + |
8019
|
|
|
(this.dayGrid ? |
8020
|
|
|
'<div class="fc-day-grid"/>' + |
8021
|
|
|
'<hr class="' + this.widgetHeaderClass + '"/>' : |
8022
|
|
|
'' |
8023
|
|
|
) + |
8024
|
|
|
'<div class="fc-time-grid-container">' + |
8025
|
|
|
'<div class="fc-time-grid"/>' + |
8026
|
|
|
'</div>' + |
8027
|
|
|
'</td>' + |
8028
|
|
|
'</tr>' + |
8029
|
|
|
'</tbody>' + |
8030
|
|
|
'</table>'; |
8031
|
|
|
}, |
8032
|
|
|
|
8033
|
|
|
|
8034
|
|
|
// Generates the HTML that will go before the day-of week header cells. |
8035
|
|
|
// Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL. |
8036
|
|
|
headIntroHtml: function() { |
8037
|
|
|
var date; |
8038
|
|
|
var weekNumber; |
8039
|
|
|
var weekTitle; |
8040
|
|
|
var weekText; |
8041
|
|
|
|
8042
|
|
|
if (this.opt('weekNumbers')) { |
8043
|
|
|
date = this.cellToDate(0, 0); |
8044
|
|
|
weekNumber = this.calendar.calculateWeekNumber(date); |
8045
|
|
|
weekTitle = this.opt('weekNumberTitle'); |
8046
|
|
|
|
8047
|
|
|
if (this.opt('isRTL')) { |
8048
|
|
|
weekText = weekNumber + weekTitle; |
8049
|
|
|
} |
8050
|
|
|
else { |
8051
|
|
|
weekText = weekTitle + weekNumber; |
8052
|
|
|
} |
8053
|
|
|
|
8054
|
|
|
return '' + |
8055
|
|
|
'<th class="fc-axis fc-week-number ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '>' + |
8056
|
|
|
'<span>' + // needed for matchCellWidths |
8057
|
|
|
htmlEscape(weekText) + |
8058
|
|
|
'</span>' + |
8059
|
|
|
'</th>'; |
8060
|
|
|
} |
8061
|
|
|
else { |
8062
|
|
|
return '<th class="fc-axis ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '></th>'; |
8063
|
|
|
} |
8064
|
|
|
}, |
8065
|
|
|
|
8066
|
|
|
|
8067
|
|
|
// Generates the HTML that goes before the all-day cells. |
8068
|
|
|
// Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL. |
8069
|
|
|
dayIntroHtml: function() { |
8070
|
|
|
return '' + |
8071
|
|
|
'<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '>' + |
8072
|
|
|
'<span>' + // needed for matchCellWidths |
8073
|
|
|
(this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) + |
8074
|
|
|
'</span>' + |
8075
|
|
|
'</td>'; |
8076
|
|
|
}, |
8077
|
|
|
|
8078
|
|
|
|
8079
|
|
|
// Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. |
8080
|
|
|
slotBgIntroHtml: function() { |
8081
|
|
|
return '<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '></td>'; |
8082
|
|
|
}, |
8083
|
|
|
|
8084
|
|
|
|
8085
|
|
|
// Generates the HTML that goes before all other types of cells. |
8086
|
|
|
// Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. |
8087
|
|
|
// Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL. |
8088
|
|
|
introHtml: function() { |
8089
|
|
|
return '<td class="fc-axis" ' + this.axisStyleAttr() + '></td>'; |
8090
|
|
|
}, |
8091
|
|
|
|
8092
|
|
|
|
8093
|
|
|
// Generates an HTML attribute string for setting the width of the axis, if it is known |
8094
|
|
|
axisStyleAttr: function() { |
8095
|
|
|
if (this.axisWidth !== null) { |
8096
|
|
|
return 'style="width:' + this.axisWidth + 'px"'; |
8097
|
|
|
} |
8098
|
|
|
return ''; |
8099
|
|
|
}, |
8100
|
|
|
|
8101
|
|
|
|
8102
|
|
|
/* Dimensions |
8103
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
8104
|
|
|
|
8105
|
|
|
updateSize: function(isResize) { |
8106
|
|
|
if (isResize) { |
8107
|
|
|
this.timeGrid.resize(); |
8108
|
|
|
} |
8109
|
|
|
View.prototype.updateSize.call(this, isResize); |
8110
|
|
|
}, |
8111
|
|
|
|
8112
|
|
|
|
8113
|
|
|
// Refreshes the horizontal dimensions of the view |
8114
|
|
|
updateWidth: function() { |
8115
|
|
|
// make all axis cells line up, and record the width so newly created axis cells will have it |
8116
|
|
|
this.axisWidth = matchCellWidths(this.el.find('.fc-axis')); |
8117
|
|
|
}, |
8118
|
|
|
|
8119
|
|
|
|
8120
|
|
|
// Adjusts the vertical dimensions of the view to the specified values |
8121
|
|
|
setHeight: function(totalHeight, isAuto) { |
8122
|
|
|
var eventLimit; |
8123
|
|
|
var scrollerHeight; |
8124
|
|
|
|
8125
|
|
|
if (this.bottomRuleHeight === null) { |
8126
|
|
|
// calculate the height of the rule the very first time |
8127
|
|
|
this.bottomRuleHeight = this.bottomRuleEl.outerHeight(); |
8128
|
|
|
} |
8129
|
|
|
this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary |
8130
|
|
|
|
8131
|
|
|
// reset all dimensions back to the original state |
8132
|
|
|
this.scrollerEl.css('overflow', ''); |
8133
|
|
|
unsetScroller(this.scrollerEl); |
8134
|
|
|
uncompensateScroll(this.noScrollRowEls); |
8135
|
|
|
|
8136
|
|
|
// limit number of events in the all-day area |
8137
|
|
|
if (this.dayGrid) { |
8138
|
|
|
this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed |
8139
|
|
|
|
8140
|
|
|
eventLimit = this.opt('eventLimit'); |
8141
|
|
|
if (eventLimit && typeof eventLimit !== 'number') { |
8142
|
|
|
eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number |
8143
|
|
|
} |
8144
|
|
|
if (eventLimit) { |
8145
|
|
|
this.dayGrid.limitRows(eventLimit); |
8146
|
|
|
} |
8147
|
|
|
} |
8148
|
|
|
|
8149
|
|
|
if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height? |
8150
|
|
|
|
8151
|
|
|
scrollerHeight = this.computeScrollerHeight(totalHeight); |
8152
|
|
|
if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? |
8153
|
|
|
|
8154
|
|
|
// make the all-day and header rows lines up |
8155
|
|
|
compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl)); |
8156
|
|
|
|
8157
|
|
|
// the scrollbar compensation might have changed text flow, which might affect height, so recalculate |
8158
|
|
|
// and reapply the desired height to the scroller. |
8159
|
|
|
scrollerHeight = this.computeScrollerHeight(totalHeight); |
8160
|
|
|
this.scrollerEl.height(scrollerHeight); |
8161
|
|
|
|
8162
|
|
|
this.restoreScroll(); |
8163
|
|
|
} |
8164
|
|
|
else { // no scrollbars |
8165
|
|
|
// still, force a height and display the bottom rule (marks the end of day) |
8166
|
|
|
this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case <hr> goes outside |
8167
|
|
|
this.bottomRuleEl.show(); |
8168
|
|
|
} |
8169
|
|
|
} |
8170
|
|
|
}, |
8171
|
|
|
|
8172
|
|
|
|
8173
|
|
|
// Sets the scroll value of the scroller to the intial pre-configured state prior to allowing the user to change it. |
8174
|
|
|
resetScroll: function() { |
8175
|
|
|
var _this = this; |
8176
|
|
|
var scrollTime = moment.duration(this.opt('scrollTime')); |
8177
|
|
|
var top = this.timeGrid.computeTimeTop(scrollTime); |
8178
|
|
|
|
8179
|
|
|
// zoom can give weird floating-point values. rather scroll a little bit further |
8180
|
|
|
top = Math.ceil(top); |
8181
|
|
|
|
8182
|
|
|
if (top) { |
8183
|
|
|
top++; // to overcome top border that slots beyond the first have. looks better |
8184
|
|
|
} |
8185
|
|
|
|
8186
|
|
|
function scroll() { |
8187
|
|
|
_this.scrollerEl.scrollTop(top); |
8188
|
|
|
} |
8189
|
|
|
|
8190
|
|
|
scroll(); |
8191
|
|
|
setTimeout(scroll, 0); // overrides any previous scroll state made by the browser |
8192
|
|
|
}, |
8193
|
|
|
|
8194
|
|
|
|
8195
|
|
|
/* Events |
8196
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
8197
|
|
|
|
8198
|
|
|
|
8199
|
|
|
// Renders events onto the view and populates the View's segment array |
8200
|
|
|
renderEvents: function(events) { |
8201
|
|
|
var dayEvents = []; |
8202
|
|
|
var timedEvents = []; |
8203
|
|
|
var daySegs = []; |
|
|
|
|
8204
|
|
|
var timedSegs; |
8205
|
|
|
var i; |
8206
|
|
|
|
8207
|
|
|
// separate the events into all-day and timed |
8208
|
|
|
for (i = 0; i < events.length; i++) { |
8209
|
|
|
if (events[i].allDay) { |
8210
|
|
|
dayEvents.push(events[i]); |
8211
|
|
|
} |
8212
|
|
|
else { |
8213
|
|
|
timedEvents.push(events[i]); |
8214
|
|
|
} |
8215
|
|
|
} |
8216
|
|
|
|
8217
|
|
|
// render the events in the subcomponents |
8218
|
|
|
timedSegs = this.timeGrid.renderEvents(timedEvents); |
|
|
|
|
8219
|
|
|
if (this.dayGrid) { |
8220
|
|
|
daySegs = this.dayGrid.renderEvents(dayEvents); |
8221
|
|
|
} |
8222
|
|
|
|
8223
|
|
|
// the all-day area is flexible and might have a lot of events, so shift the height |
8224
|
|
|
this.updateHeight(); |
8225
|
|
|
|
8226
|
|
|
View.prototype.renderEvents.call(this, events); // call the super-method |
8227
|
|
|
}, |
8228
|
|
|
|
8229
|
|
|
|
8230
|
|
|
// Retrieves all segment objects that are rendered in the view |
8231
|
|
|
getSegs: function() { |
8232
|
|
|
return this.timeGrid.getSegs().concat( |
8233
|
|
|
this.dayGrid ? this.dayGrid.getSegs() : [] |
8234
|
|
|
); |
8235
|
|
|
}, |
8236
|
|
|
|
8237
|
|
|
|
8238
|
|
|
// Unrenders all event elements and clears internal segment data |
8239
|
|
|
destroyEvents: function() { |
8240
|
|
|
View.prototype.destroyEvents.call(this); // do this before the grids' segs have been cleared |
8241
|
|
|
|
8242
|
|
|
// if destroyEvents is being called as part of an event rerender, renderEvents will be called shortly |
8243
|
|
|
// after, so remember what the scroll value was so we can restore it. |
8244
|
|
|
this.recordScroll(); |
8245
|
|
|
|
8246
|
|
|
// destroy the events in the subcomponents |
8247
|
|
|
this.timeGrid.destroyEvents(); |
8248
|
|
|
if (this.dayGrid) { |
8249
|
|
|
this.dayGrid.destroyEvents(); |
8250
|
|
|
} |
8251
|
|
|
|
8252
|
|
|
// we DON'T need to call updateHeight() because: |
8253
|
|
|
// A) a renderEvents() call always happens after this, which will eventually call updateHeight() |
8254
|
|
|
// B) in IE8, this causes a flash whenever events are rerendered |
8255
|
|
|
}, |
8256
|
|
|
|
8257
|
|
|
|
8258
|
|
|
/* Event Dragging |
8259
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
8260
|
|
|
|
8261
|
|
|
|
8262
|
|
|
// Renders a visual indication of an event being dragged over the view. |
8263
|
|
|
// A returned value of `true` signals that a mock "helper" event has been rendered. |
8264
|
|
|
renderDrag: function(start, end, seg) { |
8265
|
|
|
if (start.hasTime()) { |
8266
|
|
|
return this.timeGrid.renderDrag(start, end, seg); |
8267
|
|
|
} |
8268
|
|
|
else if (this.dayGrid) { |
8269
|
|
|
return this.dayGrid.renderDrag(start, end, seg); |
8270
|
|
|
} |
8271
|
|
|
}, |
8272
|
|
|
|
8273
|
|
|
|
8274
|
|
|
// Unrenders a visual indications of an event being dragged over the view |
8275
|
|
|
destroyDrag: function() { |
8276
|
|
|
this.timeGrid.destroyDrag(); |
8277
|
|
|
if (this.dayGrid) { |
8278
|
|
|
this.dayGrid.destroyDrag(); |
8279
|
|
|
} |
8280
|
|
|
}, |
8281
|
|
|
|
8282
|
|
|
|
8283
|
|
|
/* Selection |
8284
|
|
|
------------------------------------------------------------------------------------------------------------------*/ |
8285
|
|
|
|
8286
|
|
|
|
8287
|
|
|
// Renders a visual indication of a selection |
8288
|
|
|
renderSelection: function(start, end) { |
8289
|
|
|
if (start.hasTime() || end.hasTime()) { |
8290
|
|
|
this.timeGrid.renderSelection(start, end); |
8291
|
|
|
} |
8292
|
|
|
else if (this.dayGrid) { |
8293
|
|
|
this.dayGrid.renderSelection(start, end); |
8294
|
|
|
} |
8295
|
|
|
}, |
8296
|
|
|
|
8297
|
|
|
|
8298
|
|
|
// Unrenders a visual indications of a selection |
8299
|
|
|
destroySelection: function() { |
8300
|
|
|
this.timeGrid.destroySelection(); |
8301
|
|
|
if (this.dayGrid) { |
8302
|
|
|
this.dayGrid.destroySelection(); |
8303
|
|
|
} |
8304
|
|
|
} |
8305
|
|
|
|
8306
|
|
|
}); |
8307
|
|
|
|
8308
|
|
|
;; |
8309
|
|
|
|
8310
|
|
|
/* A week view with an all-day cell area at the top, and a time grid below |
8311
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
8312
|
|
|
// TODO: a WeekView mixin for calculating dates and titles |
8313
|
|
|
|
8314
|
|
|
fcViews.agendaWeek = AgendaWeekView; // register the view |
8315
|
|
|
|
8316
|
|
|
function AgendaWeekView(calendar) { |
8317
|
|
|
AgendaView.call(this, calendar); // call the super-constructor |
8318
|
|
|
} |
8319
|
|
|
|
8320
|
|
|
|
8321
|
|
|
AgendaWeekView.prototype = createObject(AgendaView.prototype); // define the super-class |
8322
|
|
|
$.extend(AgendaWeekView.prototype, { |
8323
|
|
|
|
8324
|
|
|
name: 'agendaWeek', |
8325
|
|
|
|
8326
|
|
|
|
8327
|
|
|
incrementDate: function(date, delta) { |
8328
|
|
|
return date.clone().stripTime().add(delta, 'weeks').startOf('week'); |
8329
|
|
|
}, |
8330
|
|
|
|
8331
|
|
|
|
8332
|
|
|
render: function(date) { |
8333
|
|
|
|
8334
|
|
|
this.intervalStart = date.clone().stripTime().startOf('week'); |
8335
|
|
|
this.intervalEnd = this.intervalStart.clone().add(1, 'weeks'); |
8336
|
|
|
|
8337
|
|
|
this.start = this.skipHiddenDays(this.intervalStart); |
8338
|
|
|
this.end = this.skipHiddenDays(this.intervalEnd, -1, true); |
8339
|
|
|
|
8340
|
|
|
this.title = this.calendar.formatRange( |
8341
|
|
|
this.start, |
8342
|
|
|
this.end.clone().subtract(1), // make inclusive by subtracting 1 ms |
8343
|
|
|
this.opt('titleFormat'), |
8344
|
|
|
' \u2014 ' // emphasized dash |
8345
|
|
|
); |
8346
|
|
|
|
8347
|
|
|
AgendaView.prototype.render.call(this, this.getCellsPerWeek()); // call the super-method |
8348
|
|
|
} |
8349
|
|
|
|
8350
|
|
|
}); |
8351
|
|
|
|
8352
|
|
|
;; |
8353
|
|
|
|
8354
|
|
|
/* A day view with an all-day cell area at the top, and a time grid below |
8355
|
|
|
----------------------------------------------------------------------------------------------------------------------*/ |
8356
|
|
|
|
8357
|
|
|
fcViews.agendaDay = AgendaDayView; // register the view |
8358
|
|
|
|
8359
|
|
|
function AgendaDayView(calendar) { |
8360
|
|
|
AgendaView.call(this, calendar); // call the super-constructor |
8361
|
|
|
} |
8362
|
|
|
|
8363
|
|
|
|
8364
|
|
|
AgendaDayView.prototype = createObject(AgendaView.prototype); // define the super-class |
8365
|
|
|
$.extend(AgendaDayView.prototype, { |
8366
|
|
|
|
8367
|
|
|
name: 'agendaDay', |
8368
|
|
|
|
8369
|
|
|
|
8370
|
|
|
incrementDate: function(date, delta) { |
8371
|
|
|
var out = date.clone().stripTime().add(delta, 'days'); |
8372
|
|
|
out = this.skipHiddenDays(out, delta < 0 ? -1 : 1); |
8373
|
|
|
return out; |
8374
|
|
|
}, |
8375
|
|
|
|
8376
|
|
|
|
8377
|
|
|
render: function(date) { |
8378
|
|
|
|
8379
|
|
|
this.start = this.intervalStart = date.clone().stripTime(); |
8380
|
|
|
this.end = this.intervalEnd = this.start.clone().add(1, 'days'); |
8381
|
|
|
|
8382
|
|
|
this.title = this.calendar.formatDate(this.start, this.opt('titleFormat')); |
8383
|
|
|
|
8384
|
|
|
AgendaView.prototype.render.call(this, 1); // call the super-method |
8385
|
|
|
} |
8386
|
|
|
|
8387
|
|
|
}); |
8388
|
|
|
|
8389
|
|
|
;; |
8390
|
|
|
|
8391
|
|
|
}); |